blob: bbf4d366fdf2b6ccf03bc6cd7aa602b1d9bb3d34 [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
James E. Blair1c236df2017-02-01 14:07:24 -080031from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070032import socket
33import string
34import subprocess
35import swiftclient
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
39import time
Clark Boylanb640e052014-04-03 16:41:46 -070040
41import git
42import gear
43import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080044import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080045import kazoo.exceptions
Clark Boylanb640e052014-04-03 16:41:46 -070046import statsd
47import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080048import testtools.content
49import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080050from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070051
James E. Blaire511d2f2016-12-08 15:22:26 -080052import zuul.driver.gerrit.gerritsource as gerritsource
53import zuul.driver.gerrit.gerritconnection as gerritconnection
Clark Boylanb640e052014-04-03 16:41:46 -070054import zuul.scheduler
55import zuul.webapp
56import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100057import zuul.launcher.server
58import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080060import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070062import zuul.merger.merger
63import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070064import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080065import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070066
67FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
68 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070069USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070070
Clark Boylanb640e052014-04-03 16:41:46 -070071
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
James E. Blairc2a5ed72017-02-20 14:12:01 -0500266 def getChangeMergedEvent(self):
267 event = {"submitter": {"name": "Jenkins",
268 "username": "jenkins"},
269 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
270 "patchSet": self.patchsets[-1],
271 "change": self.data,
272 "type": "change-merged",
273 "eventCreatedOn": 1487613810}
274 return event
275
Joshua Hesketh642824b2014-07-01 17:54:59 +1000276 def addApproval(self, category, value, username='reviewer_john',
277 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700278 if not granted_on:
279 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000280 approval = {
281 'description': self.categories[category][0],
282 'type': category,
283 'value': str(value),
284 'by': {
285 'username': username,
286 'email': username + '@example.com',
287 },
288 'grantedOn': int(granted_on)
289 }
Clark Boylanb640e052014-04-03 16:41:46 -0700290 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
291 if x['by']['username'] == username and x['type'] == category:
292 del self.patchsets[-1]['approvals'][i]
293 self.patchsets[-1]['approvals'].append(approval)
294 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000295 'author': {'email': 'author@example.com',
296 'name': 'Patchset Author',
297 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700298 'change': {'branch': self.branch,
299 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
300 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000301 'owner': {'email': 'owner@example.com',
302 'name': 'Change Owner',
303 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700304 'project': self.project,
305 'subject': self.subject,
306 'topic': 'master',
307 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000308 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700309 'patchSet': self.patchsets[-1],
310 'type': 'comment-added'}
311 self.data['submitRecords'] = self.getSubmitRecords()
312 return json.loads(json.dumps(event))
313
314 def getSubmitRecords(self):
315 status = {}
316 for cat in self.categories.keys():
317 status[cat] = 0
318
319 for a in self.patchsets[-1]['approvals']:
320 cur = status[a['type']]
321 cat_min, cat_max = self.categories[a['type']][1:]
322 new = int(a['value'])
323 if new == cat_min:
324 cur = new
325 elif abs(new) > abs(cur):
326 cur = new
327 status[a['type']] = cur
328
329 labels = []
330 ok = True
331 for typ, cat in self.categories.items():
332 cur = status[typ]
333 cat_min, cat_max = cat[1:]
334 if cur == cat_min:
335 value = 'REJECT'
336 ok = False
337 elif cur == cat_max:
338 value = 'OK'
339 else:
340 value = 'NEED'
341 ok = False
342 labels.append({'label': cat[0], 'status': value})
343 if ok:
344 return [{'status': 'OK'}]
345 return [{'status': 'NOT_READY',
346 'labels': labels}]
347
348 def setDependsOn(self, other, patchset):
349 self.depends_on_change = other
350 d = {'id': other.data['id'],
351 'number': other.data['number'],
352 'ref': other.patchsets[patchset - 1]['ref']
353 }
354 self.data['dependsOn'] = [d]
355
356 other.needed_by_changes.append(self)
357 needed = other.data.get('neededBy', [])
358 d = {'id': self.data['id'],
359 'number': self.data['number'],
360 'ref': self.patchsets[patchset - 1]['ref'],
361 'revision': self.patchsets[patchset - 1]['revision']
362 }
363 needed.append(d)
364 other.data['neededBy'] = needed
365
366 def query(self):
367 self.queried += 1
368 d = self.data.get('dependsOn')
369 if d:
370 d = d[0]
371 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
372 d['isCurrentPatchSet'] = True
373 else:
374 d['isCurrentPatchSet'] = False
375 return json.loads(json.dumps(self.data))
376
377 def setMerged(self):
378 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000379 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700380 return
381 if self.fail_merge:
382 return
383 self.data['status'] = 'MERGED'
384 self.open = False
385
386 path = os.path.join(self.upstream_root, self.project)
387 repo = git.Repo(path)
388 repo.heads[self.branch].commit = \
389 repo.commit(self.patchsets[-1]['revision'])
390
391 def setReported(self):
392 self.reported += 1
393
394
James E. Blaire511d2f2016-12-08 15:22:26 -0800395class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700396 """A Fake Gerrit connection for use in tests.
397
398 This subclasses
399 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
400 ability for tests to add changes to the fake Gerrit it represents.
401 """
402
Joshua Hesketh352264b2015-08-11 23:42:08 +1000403 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700404
James E. Blaire511d2f2016-12-08 15:22:26 -0800405 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700406 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800407 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000408 connection_config)
409
James E. Blair7fc8daa2016-08-08 15:37:15 -0700410 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700411 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
412 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000413 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700414 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200415 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700416
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700417 def addFakeChange(self, project, branch, subject, status='NEW',
418 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700419 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700420 self.change_number += 1
421 c = FakeChange(self, self.change_number, project, branch, subject,
422 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700423 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700424 self.changes[self.change_number] = c
425 return c
426
Clark Boylanb640e052014-04-03 16:41:46 -0700427 def review(self, project, changeid, message, action):
428 number, ps = changeid.split(',')
429 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000430
431 # Add the approval back onto the change (ie simulate what gerrit would
432 # do).
433 # Usually when zuul leaves a review it'll create a feedback loop where
434 # zuul's review enters another gerrit event (which is then picked up by
435 # zuul). However, we can't mimic this behaviour (by adding this
436 # approval event into the queue) as it stops jobs from checking what
437 # happens before this event is triggered. If a job needs to see what
438 # happens they can add their own verified event into the queue.
439 # Nevertheless, we can update change with the new review in gerrit.
440
James E. Blair8b5408c2016-08-08 15:37:46 -0700441 for cat in action.keys():
442 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000443 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000444
James E. Blair8b5408c2016-08-08 15:37:46 -0700445 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000446 if 'label' in action:
447 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000448 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000449
Clark Boylanb640e052014-04-03 16:41:46 -0700450 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000451
Clark Boylanb640e052014-04-03 16:41:46 -0700452 if 'submit' in action:
453 change.setMerged()
454 if message:
455 change.setReported()
456
457 def query(self, number):
458 change = self.changes.get(int(number))
459 if change:
460 return change.query()
461 return {}
462
James E. Blairc494d542014-08-06 09:23:52 -0700463 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700464 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700465 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800466 if query.startswith('change:'):
467 # Query a specific changeid
468 changeid = query[len('change:'):]
469 l = [change.query() for change in self.changes.values()
470 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700471 elif query.startswith('message:'):
472 # Query the content of a commit message
473 msg = query[len('message:'):].strip()
474 l = [change.query() for change in self.changes.values()
475 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800476 else:
477 # Query all open changes
478 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700479 return l
James E. Blairc494d542014-08-06 09:23:52 -0700480
Joshua Hesketh352264b2015-08-11 23:42:08 +1000481 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700482 pass
483
Joshua Hesketh352264b2015-08-11 23:42:08 +1000484 def getGitUrl(self, project):
485 return os.path.join(self.upstream_root, project.name)
486
Clark Boylanb640e052014-04-03 16:41:46 -0700487
488class BuildHistory(object):
489 def __init__(self, **kw):
490 self.__dict__.update(kw)
491
492 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700493 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
494 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700495
496
497class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200498 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700499 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700500 self.url = url
501
502 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700503 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700504 path = res.path
505 project = '/'.join(path.split('/')[2:-2])
506 ret = '001e# service=git-upload-pack\n'
507 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
508 'multi_ack thin-pack side-band side-band-64k ofs-delta '
509 'shallow no-progress include-tag multi_ack_detailed no-done\n')
510 path = os.path.join(self.upstream_root, project)
511 repo = git.Repo(path)
512 for ref in repo.refs:
513 r = ref.object.hexsha + ' ' + ref.path + '\n'
514 ret += '%04x%s' % (len(r) + 4, r)
515 ret += '0000'
516 return ret
517
518
Clark Boylanb640e052014-04-03 16:41:46 -0700519class FakeStatsd(threading.Thread):
520 def __init__(self):
521 threading.Thread.__init__(self)
522 self.daemon = True
523 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
524 self.sock.bind(('', 0))
525 self.port = self.sock.getsockname()[1]
526 self.wake_read, self.wake_write = os.pipe()
527 self.stats = []
528
529 def run(self):
530 while True:
531 poll = select.poll()
532 poll.register(self.sock, select.POLLIN)
533 poll.register(self.wake_read, select.POLLIN)
534 ret = poll.poll()
535 for (fd, event) in ret:
536 if fd == self.sock.fileno():
537 data = self.sock.recvfrom(1024)
538 if not data:
539 return
540 self.stats.append(data[0])
541 if fd == self.wake_read:
542 return
543
544 def stop(self):
545 os.write(self.wake_write, '1\n')
546
547
James E. Blaire1767bc2016-08-02 10:00:27 -0700548class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700549 log = logging.getLogger("zuul.test")
550
James E. Blair34776ee2016-08-25 13:53:54 -0700551 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700552 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700553 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700554 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700555 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700556 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700557 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700558 # TODOv3(jeblair): self.node is really "the image of the node
559 # assigned". We should rename it (self.node_image?) if we
560 # keep using it like this, or we may end up exposing more of
561 # the complexity around multi-node jobs here
562 # (self.nodes[0].image?)
563 self.node = None
564 if len(self.parameters.get('nodes')) == 1:
565 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700566 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100567 self.pipeline = self.parameters['ZUUL_PIPELINE']
568 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700569 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700570 self.wait_condition = threading.Condition()
571 self.waiting = False
572 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500573 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700574 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700575 self.changes = None
576 if 'ZUUL_CHANGE_IDS' in self.parameters:
577 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700578
James E. Blair3158e282016-08-19 09:34:11 -0700579 def __repr__(self):
580 waiting = ''
581 if self.waiting:
582 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100583 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
584 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700585
Clark Boylanb640e052014-04-03 16:41:46 -0700586 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700587 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700588 self.wait_condition.acquire()
589 self.wait_condition.notify()
590 self.waiting = False
591 self.log.debug("Build %s released" % self.unique)
592 self.wait_condition.release()
593
594 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700595 """Return whether this build is being held.
596
597 :returns: Whether the build is being held.
598 :rtype: bool
599 """
600
Clark Boylanb640e052014-04-03 16:41:46 -0700601 self.wait_condition.acquire()
602 if self.waiting:
603 ret = True
604 else:
605 ret = False
606 self.wait_condition.release()
607 return ret
608
609 def _wait(self):
610 self.wait_condition.acquire()
611 self.waiting = True
612 self.log.debug("Build %s waiting" % self.unique)
613 self.wait_condition.wait()
614 self.wait_condition.release()
615
616 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700617 self.log.debug('Running build %s' % self.unique)
618
James E. Blaire1767bc2016-08-02 10:00:27 -0700619 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700620 self.log.debug('Holding build %s' % self.unique)
621 self._wait()
622 self.log.debug("Build %s continuing" % self.unique)
623
James E. Blair412fba82017-01-26 15:00:50 -0800624 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700625 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800626 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700627 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800628 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500629 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800630 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700631
James E. Blaire1767bc2016-08-02 10:00:27 -0700632 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700633
James E. Blaira5dba232016-08-08 15:53:24 -0700634 def shouldFail(self):
635 changes = self.launch_server.fail_tests.get(self.name, [])
636 for change in changes:
637 if self.hasChanges(change):
638 return True
639 return False
640
James E. Blaire7b99a02016-08-05 14:27:34 -0700641 def hasChanges(self, *changes):
642 """Return whether this build has certain changes in its git repos.
643
644 :arg FakeChange changes: One or more changes (varargs) that
645 are expected to be present (in order) in the git repository of
646 the active project.
647
648 :returns: Whether the build has the indicated changes.
649 :rtype: bool
650
651 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800652 for change in changes:
653 path = os.path.join(self.jobdir.git_root, change.project)
654 try:
655 repo = git.Repo(path)
656 except NoSuchPathError as e:
657 self.log.debug('%s' % e)
658 return False
659 ref = self.parameters['ZUUL_REF']
660 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
661 commit_message = '%s-1' % change.subject
662 self.log.debug("Checking if build %s has changes; commit_message "
663 "%s; repo_messages %s" % (self, commit_message,
664 repo_messages))
665 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700666 self.log.debug(" messages do not match")
667 return False
668 self.log.debug(" OK")
669 return True
670
Clark Boylanb640e052014-04-03 16:41:46 -0700671
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000672class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700673 """An Ansible launcher to be used in tests.
674
675 :ivar bool hold_jobs_in_build: If true, when jobs are launched
676 they will report that they have started but then pause until
677 released before reporting completion. This attribute may be
678 changed at any time and will take effect for subsequently
679 launched builds, but previously held builds will still need to
680 be explicitly released.
681
682 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800683 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700684 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800685 self._test_root = kw.pop('_test_root', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800686 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700687 self.hold_jobs_in_build = False
688 self.lock = threading.Lock()
689 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700690 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700691 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700692 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800693
James E. Blaira5dba232016-08-08 15:53:24 -0700694 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700695 """Instruct the launcher to report matching builds as failures.
696
697 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700698 :arg Change change: The :py:class:`~tests.base.FakeChange`
699 instance which should cause the job to fail. This job
700 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700701
702 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700703 l = self.fail_tests.get(name, [])
704 l.append(change)
705 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800706
James E. Blair962220f2016-08-03 11:22:38 -0700707 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700708 """Release a held build.
709
710 :arg str regex: A regular expression which, if supplied, will
711 cause only builds with matching names to be released. If
712 not supplied, all builds will be released.
713
714 """
James E. Blair962220f2016-08-03 11:22:38 -0700715 builds = self.running_builds[:]
716 self.log.debug("Releasing build %s (%s)" % (regex,
717 len(self.running_builds)))
718 for build in builds:
719 if not regex or re.match(regex, build.name):
720 self.log.debug("Releasing build %s" %
721 (build.parameters['ZUUL_UUID']))
722 build.release()
723 else:
724 self.log.debug("Not releasing build %s" %
725 (build.parameters['ZUUL_UUID']))
726 self.log.debug("Done releasing builds %s (%s)" %
727 (regex, len(self.running_builds)))
728
James E. Blair17302972016-08-10 16:11:42 -0700729 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700730 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700731 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700732 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700733 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800734 args = json.loads(job.arguments)
735 args['zuul']['_test'] = dict(test_root=self._test_root)
736 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100737 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
738 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700739
740 def stopJob(self, job):
741 self.log.debug("handle stop")
742 parameters = json.loads(job.arguments)
743 uuid = parameters['uuid']
744 for build in self.running_builds:
745 if build.unique == uuid:
746 build.aborted = True
747 build.release()
748 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700749
Joshua Hesketh50c21782016-10-13 21:34:14 +1100750
751class RecordingAnsibleJob(zuul.launcher.server.AnsibleJob):
James E. Blair412fba82017-01-26 15:00:50 -0800752 def runPlaybooks(self):
Joshua Hesketh50c21782016-10-13 21:34:14 +1100753 build = self.launcher_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800754 build.jobdir = self.jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700755
James E. Blair412fba82017-01-26 15:00:50 -0800756 result = super(RecordingAnsibleJob, self).runPlaybooks()
757
Joshua Hesketh50c21782016-10-13 21:34:14 +1100758 self.launcher_server.lock.acquire()
759 self.launcher_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700760 BuildHistory(name=build.name, result=result, changes=build.changes,
761 node=build.node, uuid=build.unique,
762 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700763 pipeline=build.parameters['ZUUL_PIPELINE'])
764 )
Joshua Hesketh50c21782016-10-13 21:34:14 +1100765 self.launcher_server.running_builds.remove(build)
766 del self.launcher_server.job_builds[self.job.unique]
767 self.launcher_server.lock.release()
James E. Blair412fba82017-01-26 15:00:50 -0800768 return result
769
Monty Taylorc231d932017-02-03 09:57:15 -0600770 def runAnsible(self, cmd, timeout, secure=False):
James E. Blair412fba82017-01-26 15:00:50 -0800771 build = self.launcher_server.job_builds[self.job.unique]
772
773 if self.launcher_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600774 result = super(RecordingAnsibleJob, self).runAnsible(
775 cmd, timeout, secure=secure)
James E. Blair412fba82017-01-26 15:00:50 -0800776 else:
777 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700778 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800779
780
Clark Boylanb640e052014-04-03 16:41:46 -0700781class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700782 """A Gearman server for use in tests.
783
784 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
785 added to the queue but will not be distributed to workers
786 until released. This attribute may be changed at any time and
787 will take effect for subsequently enqueued jobs, but
788 previously held jobs will still need to be explicitly
789 released.
790
791 """
792
Clark Boylanb640e052014-04-03 16:41:46 -0700793 def __init__(self):
794 self.hold_jobs_in_queue = False
795 super(FakeGearmanServer, self).__init__(0)
796
797 def getJobForConnection(self, connection, peek=False):
798 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
799 for job in queue:
800 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500801 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700802 job.waiting = self.hold_jobs_in_queue
803 else:
804 job.waiting = False
805 if job.waiting:
806 continue
807 if job.name in connection.functions:
808 if not peek:
809 queue.remove(job)
810 connection.related_jobs[job.handle] = job
811 job.worker_connection = connection
812 job.running = True
813 return job
814 return None
815
816 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700817 """Release a held job.
818
819 :arg str regex: A regular expression which, if supplied, will
820 cause only jobs with matching names to be released. If
821 not supplied, all jobs will be released.
822 """
Clark Boylanb640e052014-04-03 16:41:46 -0700823 released = False
824 qlen = (len(self.high_queue) + len(self.normal_queue) +
825 len(self.low_queue))
826 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
827 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500828 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700829 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500830 parameters = json.loads(job.arguments)
831 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700832 self.log.debug("releasing queued job %s" %
833 job.unique)
834 job.waiting = False
835 released = True
836 else:
837 self.log.debug("not releasing queued job %s" %
838 job.unique)
839 if released:
840 self.wakeConnections()
841 qlen = (len(self.high_queue) + len(self.normal_queue) +
842 len(self.low_queue))
843 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
844
845
846class FakeSMTP(object):
847 log = logging.getLogger('zuul.FakeSMTP')
848
849 def __init__(self, messages, server, port):
850 self.server = server
851 self.port = port
852 self.messages = messages
853
854 def sendmail(self, from_email, to_email, msg):
855 self.log.info("Sending email from %s, to %s, with msg %s" % (
856 from_email, to_email, msg))
857
858 headers = msg.split('\n\n', 1)[0]
859 body = msg.split('\n\n', 1)[1]
860
861 self.messages.append(dict(
862 from_email=from_email,
863 to_email=to_email,
864 msg=msg,
865 headers=headers,
866 body=body,
867 ))
868
869 return True
870
871 def quit(self):
872 return True
873
874
875class FakeSwiftClientConnection(swiftclient.client.Connection):
876 def post_account(self, headers):
877 # Do nothing
878 pass
879
880 def get_auth(self):
881 # Returns endpoint and (unused) auth token
882 endpoint = os.path.join('https://storage.example.org', 'V1',
883 'AUTH_account')
884 return endpoint, ''
885
886
James E. Blairdce6cea2016-12-20 16:45:32 -0800887class FakeNodepool(object):
888 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800889 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800890
891 log = logging.getLogger("zuul.test.FakeNodepool")
892
893 def __init__(self, host, port, chroot):
894 self.client = kazoo.client.KazooClient(
895 hosts='%s:%s%s' % (host, port, chroot))
896 self.client.start()
897 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800898 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800899 self.thread = threading.Thread(target=self.run)
900 self.thread.daemon = True
901 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800902 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800903
904 def stop(self):
905 self._running = False
906 self.thread.join()
907 self.client.stop()
908 self.client.close()
909
910 def run(self):
911 while self._running:
912 self._run()
913 time.sleep(0.1)
914
915 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800916 if self.paused:
917 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800918 for req in self.getNodeRequests():
919 self.fulfillRequest(req)
920
921 def getNodeRequests(self):
922 try:
923 reqids = self.client.get_children(self.REQUEST_ROOT)
924 except kazoo.exceptions.NoNodeError:
925 return []
926 reqs = []
927 for oid in sorted(reqids):
928 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800929 try:
930 data, stat = self.client.get(path)
931 data = json.loads(data)
932 data['_oid'] = oid
933 reqs.append(data)
934 except kazoo.exceptions.NoNodeError:
935 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800936 return reqs
937
James E. Blaire18d4602017-01-05 11:17:28 -0800938 def getNodes(self):
939 try:
940 nodeids = self.client.get_children(self.NODE_ROOT)
941 except kazoo.exceptions.NoNodeError:
942 return []
943 nodes = []
944 for oid in sorted(nodeids):
945 path = self.NODE_ROOT + '/' + oid
946 data, stat = self.client.get(path)
947 data = json.loads(data)
948 data['_oid'] = oid
949 try:
950 lockfiles = self.client.get_children(path + '/lock')
951 except kazoo.exceptions.NoNodeError:
952 lockfiles = []
953 if lockfiles:
954 data['_lock'] = True
955 else:
956 data['_lock'] = False
957 nodes.append(data)
958 return nodes
959
James E. Blaira38c28e2017-01-04 10:33:20 -0800960 def makeNode(self, request_id, node_type):
961 now = time.time()
962 path = '/nodepool/nodes/'
963 data = dict(type=node_type,
964 provider='test-provider',
965 region='test-region',
966 az=None,
967 public_ipv4='127.0.0.1',
968 private_ipv4=None,
969 public_ipv6=None,
970 allocated_to=request_id,
971 state='ready',
972 state_time=now,
973 created_time=now,
974 updated_time=now,
975 image_id=None,
976 launcher='fake-nodepool')
977 data = json.dumps(data)
978 path = self.client.create(path, data,
979 makepath=True,
980 sequence=True)
981 nodeid = path.split("/")[-1]
982 return nodeid
983
James E. Blair6ab79e02017-01-06 10:10:17 -0800984 def addFailRequest(self, request):
985 self.fail_requests.add(request['_oid'])
986
James E. Blairdce6cea2016-12-20 16:45:32 -0800987 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -0800988 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -0800989 return
990 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800991 oid = request['_oid']
992 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800993
James E. Blair6ab79e02017-01-06 10:10:17 -0800994 if oid in self.fail_requests:
995 request['state'] = 'failed'
996 else:
997 request['state'] = 'fulfilled'
998 nodes = []
999 for node in request['node_types']:
1000 nodeid = self.makeNode(oid, node)
1001 nodes.append(nodeid)
1002 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001003
James E. Blaira38c28e2017-01-04 10:33:20 -08001004 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001005 path = self.REQUEST_ROOT + '/' + oid
1006 data = json.dumps(request)
1007 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1008 self.client.set(path, data)
1009
1010
James E. Blair498059b2016-12-20 13:50:13 -08001011class ChrootedKazooFixture(fixtures.Fixture):
1012 def __init__(self):
1013 super(ChrootedKazooFixture, self).__init__()
1014
1015 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1016 if ':' in zk_host:
1017 host, port = zk_host.split(':')
1018 else:
1019 host = zk_host
1020 port = None
1021
1022 self.zookeeper_host = host
1023
1024 if not port:
1025 self.zookeeper_port = 2181
1026 else:
1027 self.zookeeper_port = int(port)
1028
1029 def _setUp(self):
1030 # Make sure the test chroot paths do not conflict
1031 random_bits = ''.join(random.choice(string.ascii_lowercase +
1032 string.ascii_uppercase)
1033 for x in range(8))
1034
1035 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1036 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1037
1038 # Ensure the chroot path exists and clean up any pre-existing znodes.
1039 _tmp_client = kazoo.client.KazooClient(
1040 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1041 _tmp_client.start()
1042
1043 if _tmp_client.exists(self.zookeeper_chroot):
1044 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1045
1046 _tmp_client.ensure_path(self.zookeeper_chroot)
1047 _tmp_client.stop()
1048 _tmp_client.close()
1049
1050 self.addCleanup(self._cleanup)
1051
1052 def _cleanup(self):
1053 '''Remove the chroot path.'''
1054 # Need a non-chroot'ed client to remove the chroot path
1055 _tmp_client = kazoo.client.KazooClient(
1056 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1057 _tmp_client.start()
1058 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1059 _tmp_client.stop()
1060
1061
Maru Newby3fe5f852015-01-13 04:22:14 +00001062class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001063 log = logging.getLogger("zuul.test")
1064
James E. Blair1c236df2017-02-01 14:07:24 -08001065 def attachLogs(self, *args):
1066 def reader():
1067 self._log_stream.seek(0)
1068 while True:
1069 x = self._log_stream.read(4096)
1070 if not x:
1071 break
1072 yield x.encode('utf8')
1073 content = testtools.content.content_from_reader(
1074 reader,
1075 testtools.content_type.UTF8_TEXT,
1076 False)
1077 self.addDetail('logging', content)
1078
Clark Boylanb640e052014-04-03 16:41:46 -07001079 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001080 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001081 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1082 try:
1083 test_timeout = int(test_timeout)
1084 except ValueError:
1085 # If timeout value is invalid do not set a timeout.
1086 test_timeout = 0
1087 if test_timeout > 0:
1088 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1089
1090 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1091 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1092 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1093 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1094 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1095 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1096 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1097 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1098 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1099 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001100 self._log_stream = StringIO()
1101 self.addOnException(self.attachLogs)
1102 else:
1103 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001104
James E. Blair1c236df2017-02-01 14:07:24 -08001105 handler = logging.StreamHandler(self._log_stream)
1106 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1107 '%(levelname)-8s %(message)s')
1108 handler.setFormatter(formatter)
1109
1110 logger = logging.getLogger()
1111 logger.setLevel(logging.DEBUG)
1112 logger.addHandler(handler)
1113
1114 # NOTE(notmorgan): Extract logging overrides for specific
1115 # libraries from the OS_LOG_DEFAULTS env and create loggers
1116 # for each. This is used to limit the output during test runs
1117 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001118 log_defaults_from_env = os.environ.get(
1119 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001120 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001121
James E. Blairdce6cea2016-12-20 16:45:32 -08001122 if log_defaults_from_env:
1123 for default in log_defaults_from_env.split(','):
1124 try:
1125 name, level_str = default.split('=', 1)
1126 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001127 logger = logging.getLogger(name)
1128 logger.setLevel(level)
1129 logger.addHandler(handler)
1130 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001131 except ValueError:
1132 # NOTE(notmorgan): Invalid format of the log default,
1133 # skip and don't try and apply a logger for the
1134 # specified module
1135 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001136
Maru Newby3fe5f852015-01-13 04:22:14 +00001137
1138class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001139 """A test case with a functioning Zuul.
1140
1141 The following class variables are used during test setup and can
1142 be overidden by subclasses but are effectively read-only once a
1143 test method starts running:
1144
1145 :cvar str config_file: This points to the main zuul config file
1146 within the fixtures directory. Subclasses may override this
1147 to obtain a different behavior.
1148
1149 :cvar str tenant_config_file: This is the tenant config file
1150 (which specifies from what git repos the configuration should
1151 be loaded). It defaults to the value specified in
1152 `config_file` but can be overidden by subclasses to obtain a
1153 different tenant/project layout while using the standard main
1154 configuration.
1155
1156 The following are instance variables that are useful within test
1157 methods:
1158
1159 :ivar FakeGerritConnection fake_<connection>:
1160 A :py:class:`~tests.base.FakeGerritConnection` will be
1161 instantiated for each connection present in the config file
1162 and stored here. For instance, `fake_gerrit` will hold the
1163 FakeGerritConnection object for a connection named `gerrit`.
1164
1165 :ivar FakeGearmanServer gearman_server: An instance of
1166 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1167 server that all of the Zuul components in this test use to
1168 communicate with each other.
1169
1170 :ivar RecordingLaunchServer launch_server: An instance of
1171 :py:class:`~tests.base.RecordingLaunchServer` which is the
1172 Ansible launch server used to run jobs for this test.
1173
1174 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1175 representing currently running builds. They are appended to
1176 the list in the order they are launched, and removed from this
1177 list upon completion.
1178
1179 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1180 objects representing completed builds. They are appended to
1181 the list in the order they complete.
1182
1183 """
1184
James E. Blair83005782015-12-11 14:46:03 -08001185 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001186 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001187
1188 def _startMerger(self):
1189 self.merge_server = zuul.merger.server.MergeServer(self.config,
1190 self.connections)
1191 self.merge_server.start()
1192
Maru Newby3fe5f852015-01-13 04:22:14 +00001193 def setUp(self):
1194 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001195
1196 self.setupZK()
1197
James E. Blair97d902e2014-08-21 13:25:56 -07001198 if USE_TEMPDIR:
1199 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001200 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1201 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001202 else:
1203 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001204 self.test_root = os.path.join(tmp_root, "zuul-test")
1205 self.upstream_root = os.path.join(self.test_root, "upstream")
James E. Blair8c1be532017-02-07 14:04:12 -08001206 self.merger_git_root = os.path.join(self.test_root, "merger-git")
1207 self.launcher_git_root = os.path.join(self.test_root, "launcher-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001208 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001209
1210 if os.path.exists(self.test_root):
1211 shutil.rmtree(self.test_root)
1212 os.makedirs(self.test_root)
1213 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001214 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001215
1216 # Make per test copy of Configuration.
1217 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001218 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001219 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001220 self.config.get('zuul', 'tenant_config')))
James E. Blair8c1be532017-02-07 14:04:12 -08001221 self.config.set('merger', 'git_dir', self.merger_git_root)
1222 self.config.set('launcher', 'git_dir', self.launcher_git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001223 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001224
1225 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001226 # TODOv3(jeblair): remove these and replace with new git
1227 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001228 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001229 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001230 self.init_repo("org/project5")
1231 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001232 self.init_repo("org/one-job-project")
1233 self.init_repo("org/nonvoting-project")
1234 self.init_repo("org/templated-project")
1235 self.init_repo("org/layered-project")
1236 self.init_repo("org/node-project")
1237 self.init_repo("org/conflict-project")
1238 self.init_repo("org/noop-project")
1239 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001240 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001241
1242 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001243 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1244 # see: https://github.com/jsocol/pystatsd/issues/61
1245 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001246 os.environ['STATSD_PORT'] = str(self.statsd.port)
1247 self.statsd.start()
1248 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001249 reload_module(statsd)
1250 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001251
1252 self.gearman_server = FakeGearmanServer()
1253
1254 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001255 self.log.info("Gearman server on port %s" %
1256 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001257
James E. Blaire511d2f2016-12-08 15:22:26 -08001258 gerritsource.GerritSource.replication_timeout = 1.5
1259 gerritsource.GerritSource.replication_retry_interval = 0.5
1260 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001261
Joshua Hesketh352264b2015-08-11 23:42:08 +10001262 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001263
1264 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1265 FakeSwiftClientConnection))
James E. Blaire511d2f2016-12-08 15:22:26 -08001266
Clark Boylanb640e052014-04-03 16:41:46 -07001267 self.swift = zuul.lib.swift.Swift(self.config)
1268
Jan Hruban6b71aff2015-10-22 16:58:08 +02001269 self.event_queues = [
1270 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001271 self.sched.trigger_event_queue,
1272 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001273 ]
1274
James E. Blairfef78942016-03-11 16:28:56 -08001275 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001276 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001277
Clark Boylanb640e052014-04-03 16:41:46 -07001278 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001279 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001280 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001281 return FakeURLOpener(self.upstream_root, *args, **kw)
1282
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001283 old_urlopen = urllib.request.urlopen
1284 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001285
James E. Blair3f876d52016-07-22 13:07:14 -07001286 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001287
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 self.launch_server = RecordingLaunchServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001289 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001290 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001291 _run_ansible=self.run_ansible,
1292 _test_root=self.test_root)
James E. Blaire1767bc2016-08-02 10:00:27 -07001293 self.launch_server.start()
1294 self.history = self.launch_server.build_history
1295 self.builds = self.launch_server.running_builds
1296
1297 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001298 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001299 self.merge_client = zuul.merger.client.MergeClient(
1300 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001301 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001302 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001303 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001304
James E. Blair0d5a36e2017-02-21 10:53:44 -05001305 self.fake_nodepool = FakeNodepool(
1306 self.zk_chroot_fixture.zookeeper_host,
1307 self.zk_chroot_fixture.zookeeper_port,
1308 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001309
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001311 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001312 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001313 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001314
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001315 self.webapp = zuul.webapp.WebApp(
1316 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001317 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001318
1319 self.sched.start()
1320 self.sched.reconfigure(self.config)
1321 self.sched.resume()
1322 self.webapp.start()
1323 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001324 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001325
Clark Boylanb640e052014-04-03 16:41:46 -07001326 self.addCleanup(self.shutdown)
1327
James E. Blaire18d4602017-01-05 11:17:28 -08001328 def tearDown(self):
1329 super(ZuulTestCase, self).tearDown()
1330 self.assertFinalState()
1331
James E. Blairfef78942016-03-11 16:28:56 -08001332 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001333 # Set up gerrit related fakes
1334 # Set a changes database so multiple FakeGerrit's can report back to
1335 # a virtual canonical database given by the configured hostname
1336 self.gerrit_changes_dbs = {}
1337
1338 def getGerritConnection(driver, name, config):
1339 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1340 con = FakeGerritConnection(driver, name, config,
1341 changes_db=db,
1342 upstream_root=self.upstream_root)
1343 self.event_queues.append(con.event_queue)
1344 setattr(self, 'fake_' + name, con)
1345 return con
1346
1347 self.useFixture(fixtures.MonkeyPatch(
1348 'zuul.driver.gerrit.GerritDriver.getConnection',
1349 getGerritConnection))
1350
1351 # Set up smtp related fakes
Joshua Hesketh352264b2015-08-11 23:42:08 +10001352 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001353
Joshua Hesketh352264b2015-08-11 23:42:08 +10001354 def FakeSMTPFactory(*args, **kw):
1355 args = [self.smtp_messages] + list(args)
1356 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001357
Joshua Hesketh352264b2015-08-11 23:42:08 +10001358 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001359
James E. Blaire511d2f2016-12-08 15:22:26 -08001360 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001361 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001362 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001363
James E. Blair83005782015-12-11 14:46:03 -08001364 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001365 # This creates the per-test configuration object. It can be
1366 # overriden by subclasses, but should not need to be since it
1367 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001368 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001369 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001370 if hasattr(self, 'tenant_config_file'):
1371 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001372 git_path = os.path.join(
1373 os.path.dirname(
1374 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1375 'git')
1376 if os.path.exists(git_path):
1377 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001378 project = reponame.replace('_', '/')
1379 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001380 os.path.join(git_path, reponame))
1381
James E. Blair498059b2016-12-20 13:50:13 -08001382 def setupZK(self):
1383 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001384 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001385 self.zk_chroot_fixture.zookeeper_host,
1386 self.zk_chroot_fixture.zookeeper_port,
1387 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001388
James E. Blair96c6bf82016-01-15 16:20:40 -08001389 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001390 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001391
1392 files = {}
1393 for (dirpath, dirnames, filenames) in os.walk(source_path):
1394 for filename in filenames:
1395 test_tree_filepath = os.path.join(dirpath, filename)
1396 common_path = os.path.commonprefix([test_tree_filepath,
1397 source_path])
1398 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1399 with open(test_tree_filepath, 'r') as f:
1400 content = f.read()
1401 files[relative_filepath] = content
1402 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001403 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001404
James E. Blaire18d4602017-01-05 11:17:28 -08001405 def assertNodepoolState(self):
1406 # Make sure that there are no pending requests
1407
1408 requests = self.fake_nodepool.getNodeRequests()
1409 self.assertEqual(len(requests), 0)
1410
1411 nodes = self.fake_nodepool.getNodes()
1412 for node in nodes:
1413 self.assertFalse(node['_lock'], "Node %s is locked" %
1414 (node['_oid'],))
1415
Clark Boylanb640e052014-04-03 16:41:46 -07001416 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001417 # Make sure that git.Repo objects have been garbage collected.
1418 repos = []
1419 gc.collect()
1420 for obj in gc.get_objects():
1421 if isinstance(obj, git.Repo):
1422 repos.append(obj)
1423 self.assertEqual(len(repos), 0)
1424 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001425 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001426 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001427 for tenant in self.sched.abide.tenants.values():
1428 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001429 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001430 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001431
1432 def shutdown(self):
1433 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001434 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001435 self.merge_server.stop()
1436 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001437 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001438 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001439 self.sched.stop()
1440 self.sched.join()
1441 self.statsd.stop()
1442 self.statsd.join()
1443 self.webapp.stop()
1444 self.webapp.join()
1445 self.rpc.stop()
1446 self.rpc.join()
1447 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001448 self.fake_nodepool.stop()
1449 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001450 threads = threading.enumerate()
1451 if len(threads) > 1:
1452 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001453 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001454
1455 def init_repo(self, project):
1456 parts = project.split('/')
1457 path = os.path.join(self.upstream_root, *parts[:-1])
1458 if not os.path.exists(path):
1459 os.makedirs(path)
1460 path = os.path.join(self.upstream_root, project)
1461 repo = git.Repo.init(path)
1462
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001463 with repo.config_writer() as config_writer:
1464 config_writer.set_value('user', 'email', 'user@example.com')
1465 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001466
Clark Boylanb640e052014-04-03 16:41:46 -07001467 repo.index.commit('initial commit')
1468 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001469
James E. Blair97d902e2014-08-21 13:25:56 -07001470 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001471 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001472 repo.git.clean('-x', '-f', '-d')
1473
James E. Blair97d902e2014-08-21 13:25:56 -07001474 def create_branch(self, project, branch):
1475 path = os.path.join(self.upstream_root, project)
1476 repo = git.Repo.init(path)
1477 fn = os.path.join(path, 'README')
1478
1479 branch_head = repo.create_head(branch)
1480 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001481 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001482 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001483 f.close()
1484 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001485 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001486
James E. Blair97d902e2014-08-21 13:25:56 -07001487 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001488 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001489 repo.git.clean('-x', '-f', '-d')
1490
Sachi King9f16d522016-03-16 12:20:45 +11001491 def create_commit(self, project):
1492 path = os.path.join(self.upstream_root, project)
1493 repo = git.Repo(path)
1494 repo.head.reference = repo.heads['master']
1495 file_name = os.path.join(path, 'README')
1496 with open(file_name, 'a') as f:
1497 f.write('creating fake commit\n')
1498 repo.index.add([file_name])
1499 commit = repo.index.commit('Creating a fake commit')
1500 return commit.hexsha
1501
James E. Blairb8c16472015-05-05 14:55:26 -07001502 def orderedRelease(self):
1503 # Run one build at a time to ensure non-race order:
1504 while len(self.builds):
1505 self.release(self.builds[0])
1506 self.waitUntilSettled()
1507
Clark Boylanb640e052014-04-03 16:41:46 -07001508 def release(self, job):
1509 if isinstance(job, FakeBuild):
1510 job.release()
1511 else:
1512 job.waiting = False
1513 self.log.debug("Queued job %s released" % job.unique)
1514 self.gearman_server.wakeConnections()
1515
1516 def getParameter(self, job, name):
1517 if isinstance(job, FakeBuild):
1518 return job.parameters[name]
1519 else:
1520 parameters = json.loads(job.arguments)
1521 return parameters[name]
1522
Clark Boylanb640e052014-04-03 16:41:46 -07001523 def haveAllBuildsReported(self):
1524 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001525 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001526 return False
1527 # Find out if every build that the worker has completed has been
1528 # reported back to Zuul. If it hasn't then that means a Gearman
1529 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001530 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001531 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001532 if not zbuild:
1533 # It has already been reported
1534 continue
1535 # It hasn't been reported yet.
1536 return False
1537 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001538 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001539 if connection.state == 'GRAB_WAIT':
1540 return False
1541 return True
1542
1543 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001544 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001545 for build in builds:
1546 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001547 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001548 for j in conn.related_jobs.values():
1549 if j.unique == build.uuid:
1550 client_job = j
1551 break
1552 if not client_job:
1553 self.log.debug("%s is not known to the gearman client" %
1554 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001555 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001556 if not client_job.handle:
1557 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001558 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001559 server_job = self.gearman_server.jobs.get(client_job.handle)
1560 if not server_job:
1561 self.log.debug("%s is not known to the gearman server" %
1562 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001563 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001564 if not hasattr(server_job, 'waiting'):
1565 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001566 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001567 if server_job.waiting:
1568 continue
James E. Blair17302972016-08-10 16:11:42 -07001569 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001570 self.log.debug("%s has not reported start" % build)
1571 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001572 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001573 if worker_build:
1574 if worker_build.isWaiting():
1575 continue
1576 else:
1577 self.log.debug("%s is running" % worker_build)
1578 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001579 else:
James E. Blair962220f2016-08-03 11:22:38 -07001580 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001581 return False
1582 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001583
James E. Blairdce6cea2016-12-20 16:45:32 -08001584 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001585 if self.fake_nodepool.paused:
1586 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001587 if self.sched.nodepool.requests:
1588 return False
1589 return True
1590
Jan Hruban6b71aff2015-10-22 16:58:08 +02001591 def eventQueuesEmpty(self):
1592 for queue in self.event_queues:
1593 yield queue.empty()
1594
1595 def eventQueuesJoin(self):
1596 for queue in self.event_queues:
1597 queue.join()
1598
Clark Boylanb640e052014-04-03 16:41:46 -07001599 def waitUntilSettled(self):
1600 self.log.debug("Waiting until settled...")
1601 start = time.time()
1602 while True:
James E. Blair71932482017-02-02 11:29:07 -08001603 if time.time() - start > 20:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001604 self.log.error("Timeout waiting for Zuul to settle")
1605 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001606 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001607 self.log.error(" %s: %s" % (queue, queue.empty()))
1608 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001609 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001610 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001611 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001612 self.log.error("All requests completed: %s" %
1613 (self.areAllNodeRequestsComplete(),))
1614 self.log.error("Merge client jobs: %s" %
1615 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001616 raise Exception("Timeout waiting for Zuul to settle")
1617 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001618
James E. Blaire1767bc2016-08-02 10:00:27 -07001619 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001620 # have all build states propogated to zuul?
1621 if self.haveAllBuildsReported():
1622 # Join ensures that the queue is empty _and_ events have been
1623 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001624 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001625 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001626 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001627 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001628 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001629 self.areAllNodeRequestsComplete() and
1630 all(self.eventQueuesEmpty())):
1631 # The queue empty check is placed at the end to
1632 # ensure that if a component adds an event between
1633 # when locked the run handler and checked that the
1634 # components were stable, we don't erroneously
1635 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001636 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001637 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001638 self.log.debug("...settled.")
1639 return
1640 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001641 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001642 self.sched.wake_event.wait(0.1)
1643
1644 def countJobResults(self, jobs, result):
1645 jobs = filter(lambda x: x.result == result, jobs)
1646 return len(jobs)
1647
James E. Blair96c6bf82016-01-15 16:20:40 -08001648 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001649 for job in self.history:
1650 if (job.name == name and
1651 (project is None or
1652 job.parameters['ZUUL_PROJECT'] == project)):
1653 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001654 raise Exception("Unable to find job %s in history" % name)
1655
1656 def assertEmptyQueues(self):
1657 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001658 for tenant in self.sched.abide.tenants.values():
1659 for pipeline in tenant.layout.pipelines.values():
1660 for queue in pipeline.queues:
1661 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001662 print('pipeline %s queue %s contents %s' % (
1663 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001664 self.assertEqual(len(queue.queue), 0,
1665 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001666
1667 def assertReportedStat(self, key, value=None, kind=None):
1668 start = time.time()
1669 while time.time() < (start + 5):
1670 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001671 k, v = stat.split(':')
1672 if key == k:
1673 if value is None and kind is None:
1674 return
1675 elif value:
1676 if value == v:
1677 return
1678 elif kind:
1679 if v.endswith('|' + kind):
1680 return
1681 time.sleep(0.1)
1682
Clark Boylanb640e052014-04-03 16:41:46 -07001683 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001684
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001685 def assertBuilds(self, builds):
1686 """Assert that the running builds are as described.
1687
1688 The list of running builds is examined and must match exactly
1689 the list of builds described by the input.
1690
1691 :arg list builds: A list of dictionaries. Each item in the
1692 list must match the corresponding build in the build
1693 history, and each element of the dictionary must match the
1694 corresponding attribute of the build.
1695
1696 """
James E. Blair3158e282016-08-19 09:34:11 -07001697 try:
1698 self.assertEqual(len(self.builds), len(builds))
1699 for i, d in enumerate(builds):
1700 for k, v in d.items():
1701 self.assertEqual(
1702 getattr(self.builds[i], k), v,
1703 "Element %i in builds does not match" % (i,))
1704 except Exception:
1705 for build in self.builds:
1706 self.log.error("Running build: %s" % build)
1707 else:
1708 self.log.error("No running builds")
1709 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001710
James E. Blairb536ecc2016-08-31 10:11:42 -07001711 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001712 """Assert that the completed builds are as described.
1713
1714 The list of completed builds is examined and must match
1715 exactly the list of builds described by the input.
1716
1717 :arg list history: A list of dictionaries. Each item in the
1718 list must match the corresponding build in the build
1719 history, and each element of the dictionary must match the
1720 corresponding attribute of the build.
1721
James E. Blairb536ecc2016-08-31 10:11:42 -07001722 :arg bool ordered: If true, the history must match the order
1723 supplied, if false, the builds are permitted to have
1724 arrived in any order.
1725
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001726 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001727 def matches(history_item, item):
1728 for k, v in item.items():
1729 if getattr(history_item, k) != v:
1730 return False
1731 return True
James E. Blair3158e282016-08-19 09:34:11 -07001732 try:
1733 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001734 if ordered:
1735 for i, d in enumerate(history):
1736 if not matches(self.history[i], d):
1737 raise Exception(
1738 "Element %i in history does not match" % (i,))
1739 else:
1740 unseen = self.history[:]
1741 for i, d in enumerate(history):
1742 found = False
1743 for unseen_item in unseen:
1744 if matches(unseen_item, d):
1745 found = True
1746 unseen.remove(unseen_item)
1747 break
1748 if not found:
1749 raise Exception("No match found for element %i "
1750 "in history" % (i,))
1751 if unseen:
1752 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001753 except Exception:
1754 for build in self.history:
1755 self.log.error("Completed build: %s" % build)
1756 else:
1757 self.log.error("No completed builds")
1758 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001759
James E. Blair6ac368c2016-12-22 18:07:20 -08001760 def printHistory(self):
1761 """Log the build history.
1762
1763 This can be useful during tests to summarize what jobs have
1764 completed.
1765
1766 """
1767 self.log.debug("Build history:")
1768 for build in self.history:
1769 self.log.debug(build)
1770
James E. Blair59fdbac2015-12-07 17:08:06 -08001771 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001772 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1773
1774 def updateConfigLayout(self, path):
1775 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001776 if not os.path.exists(root):
1777 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001778 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1779 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001780- tenant:
1781 name: openstack
1782 source:
1783 gerrit:
1784 config-repos:
1785 - %s
1786 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001787 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001788 self.config.set('zuul', 'tenant_config',
1789 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001790
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001791 def addCommitToRepo(self, project, message, files,
1792 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001793 path = os.path.join(self.upstream_root, project)
1794 repo = git.Repo(path)
1795 repo.head.reference = branch
1796 zuul.merger.merger.reset_repo_to_head(repo)
1797 for fn, content in files.items():
1798 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001799 try:
1800 os.makedirs(os.path.dirname(fn))
1801 except OSError:
1802 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001803 with open(fn, 'w') as f:
1804 f.write(content)
1805 repo.index.add([fn])
1806 commit = repo.index.commit(message)
1807 repo.heads[branch].commit = commit
1808 repo.head.reference = branch
1809 repo.git.clean('-x', '-f', '-d')
1810 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001811 if tag:
1812 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001813
James E. Blair7fc8daa2016-08-08 15:37:15 -07001814 def addEvent(self, connection, event):
1815 """Inject a Fake (Gerrit) event.
1816
1817 This method accepts a JSON-encoded event and simulates Zuul
1818 having received it from Gerrit. It could (and should)
1819 eventually apply to any connection type, but is currently only
1820 used with Gerrit connections. The name of the connection is
1821 used to look up the corresponding server, and the event is
1822 simulated as having been received by all Zuul connections
1823 attached to that server. So if two Gerrit connections in Zuul
1824 are connected to the same Gerrit server, and you invoke this
1825 method specifying the name of one of them, the event will be
1826 received by both.
1827
1828 .. note::
1829
1830 "self.fake_gerrit.addEvent" calls should be migrated to
1831 this method.
1832
1833 :arg str connection: The name of the connection corresponding
1834 to the gerrit server.
1835 :arg str event: The JSON-encoded event.
1836
1837 """
1838 specified_conn = self.connections.connections[connection]
1839 for conn in self.connections.connections.values():
1840 if (isinstance(conn, specified_conn.__class__) and
1841 specified_conn.server == conn.server):
1842 conn.addEvent(event)
1843
James E. Blair3f876d52016-07-22 13:07:14 -07001844
1845class AnsibleZuulTestCase(ZuulTestCase):
1846 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001847 run_ansible = True