blob: f10157db9cc43ce782a6c1e44cbe6af82dd47320 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070031import socket
32import string
33import subprocess
34import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080035import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070036import threading
37import time
Clark Boylanb640e052014-04-03 16:41:46 -070038
39import git
40import gear
41import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080042import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080043import kazoo.exceptions
Clark Boylanb640e052014-04-03 16:41:46 -070044import statsd
45import testtools
Clint Byrum3343e3e2016-11-15 16:05:03 -080046from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070047
Joshua Hesketh352264b2015-08-11 23:42:08 +100048import zuul.connection.gerrit
49import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070050import zuul.scheduler
51import zuul.webapp
52import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100053import zuul.launcher.server
54import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080056import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070057import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070058import zuul.merger.merger
59import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070060import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.reporter.gerrit
62import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110063import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.trigger.gerrit
65import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070066import zuul.trigger.zuultrigger
James E. Blairdce6cea2016-12-20 16:45:32 -080067import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070068
69FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
70 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070071USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070072
73logging.basicConfig(level=logging.DEBUG,
74 format='%(asctime)s %(name)-32s '
75 '%(levelname)-8s %(message)s')
76
77
78def repack_repo(path):
79 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
80 output = subprocess.Popen(cmd, close_fds=True,
81 stdout=subprocess.PIPE,
82 stderr=subprocess.PIPE)
83 out = output.communicate()
84 if output.returncode:
85 raise Exception("git repack returned %d" % output.returncode)
86 return out
87
88
89def random_sha1():
90 return hashlib.sha1(str(random.random())).hexdigest()
91
92
James E. Blaira190f3b2015-01-05 14:56:54 -080093def iterate_timeout(max_seconds, purpose):
94 start = time.time()
95 count = 0
96 while (time.time() < start + max_seconds):
97 count += 1
98 yield count
99 time.sleep(0)
100 raise Exception("Timeout waiting for %s" % purpose)
101
102
Clark Boylanb640e052014-04-03 16:41:46 -0700103class ChangeReference(git.Reference):
104 _common_path_default = "refs/changes"
105 _points_to_commits_only = True
106
107
108class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700109 categories = {'approved': ('Approved', -1, 1),
110 'code-review': ('Code-Review', -2, 2),
111 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700112
113 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700114 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700115 self.gerrit = gerrit
116 self.reported = 0
117 self.queried = 0
118 self.patchsets = []
119 self.number = number
120 self.project = project
121 self.branch = branch
122 self.subject = subject
123 self.latest_patchset = 0
124 self.depends_on_change = None
125 self.needed_by_changes = []
126 self.fail_merge = False
127 self.messages = []
128 self.data = {
129 'branch': branch,
130 'comments': [],
131 'commitMessage': subject,
132 'createdOn': time.time(),
133 'id': 'I' + random_sha1(),
134 'lastUpdated': time.time(),
135 'number': str(number),
136 'open': status == 'NEW',
137 'owner': {'email': 'user@example.com',
138 'name': 'User Name',
139 'username': 'username'},
140 'patchSets': self.patchsets,
141 'project': project,
142 'status': status,
143 'subject': subject,
144 'submitRecords': [],
145 'url': 'https://hostname/%s' % number}
146
147 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700148 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700149 self.data['submitRecords'] = self.getSubmitRecords()
150 self.open = status == 'NEW'
151
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700152 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700153 path = os.path.join(self.upstream_root, self.project)
154 repo = git.Repo(path)
155 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
156 self.latest_patchset),
157 'refs/tags/init')
158 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700159 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700160 repo.git.clean('-x', '-f', '-d')
161
162 path = os.path.join(self.upstream_root, self.project)
163 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700164 for fn, content in files.items():
165 fn = os.path.join(path, fn)
166 with open(fn, 'w') as f:
167 f.write(content)
168 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700169 else:
170 for fni in range(100):
171 fn = os.path.join(path, str(fni))
172 f = open(fn, 'w')
173 for ci in range(4096):
174 f.write(random.choice(string.printable))
175 f.close()
176 repo.index.add([fn])
177
178 r = repo.index.commit(msg)
179 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700180 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700181 repo.git.clean('-x', '-f', '-d')
182 repo.heads['master'].checkout()
183 return r
184
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700185 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700186 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700187 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700188 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700189 data = ("test %s %s %s\n" %
190 (self.branch, self.number, self.latest_patchset))
191 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700192 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700194 ps_files = [{'file': '/COMMIT_MSG',
195 'type': 'ADDED'},
196 {'file': 'README',
197 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700198 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700199 ps_files.append({'file': f, 'type': 'ADDED'})
200 d = {'approvals': [],
201 'createdOn': time.time(),
202 'files': ps_files,
203 'number': str(self.latest_patchset),
204 'ref': 'refs/changes/1/%s/%s' % (self.number,
205 self.latest_patchset),
206 'revision': c.hexsha,
207 'uploader': {'email': 'user@example.com',
208 'name': 'User name',
209 'username': 'user'}}
210 self.data['currentPatchSet'] = d
211 self.patchsets.append(d)
212 self.data['submitRecords'] = self.getSubmitRecords()
213
214 def getPatchsetCreatedEvent(self, patchset):
215 event = {"type": "patchset-created",
216 "change": {"project": self.project,
217 "branch": self.branch,
218 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
219 "number": str(self.number),
220 "subject": self.subject,
221 "owner": {"name": "User Name"},
222 "url": "https://hostname/3"},
223 "patchSet": self.patchsets[patchset - 1],
224 "uploader": {"name": "User Name"}}
225 return event
226
227 def getChangeRestoredEvent(self):
228 event = {"type": "change-restored",
229 "change": {"project": self.project,
230 "branch": self.branch,
231 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
232 "number": str(self.number),
233 "subject": self.subject,
234 "owner": {"name": "User Name"},
235 "url": "https://hostname/3"},
236 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100237 "patchSet": self.patchsets[-1],
238 "reason": ""}
239 return event
240
241 def getChangeAbandonedEvent(self):
242 event = {"type": "change-abandoned",
243 "change": {"project": self.project,
244 "branch": self.branch,
245 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
246 "number": str(self.number),
247 "subject": self.subject,
248 "owner": {"name": "User Name"},
249 "url": "https://hostname/3"},
250 "abandoner": {"name": "User Name"},
251 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700252 "reason": ""}
253 return event
254
255 def getChangeCommentEvent(self, patchset):
256 event = {"type": "comment-added",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "patchSet": self.patchsets[patchset - 1],
265 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700266 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700267 "description": "Code-Review",
268 "value": "0"}],
269 "comment": "This is a comment"}
270 return event
271
Joshua Hesketh642824b2014-07-01 17:54:59 +1000272 def addApproval(self, category, value, username='reviewer_john',
273 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700274 if not granted_on:
275 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000276 approval = {
277 'description': self.categories[category][0],
278 'type': category,
279 'value': str(value),
280 'by': {
281 'username': username,
282 'email': username + '@example.com',
283 },
284 'grantedOn': int(granted_on)
285 }
Clark Boylanb640e052014-04-03 16:41:46 -0700286 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
287 if x['by']['username'] == username and x['type'] == category:
288 del self.patchsets[-1]['approvals'][i]
289 self.patchsets[-1]['approvals'].append(approval)
290 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000291 'author': {'email': 'author@example.com',
292 'name': 'Patchset Author',
293 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700294 'change': {'branch': self.branch,
295 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
296 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000297 'owner': {'email': 'owner@example.com',
298 'name': 'Change Owner',
299 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700300 'project': self.project,
301 'subject': self.subject,
302 'topic': 'master',
303 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000304 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700305 'patchSet': self.patchsets[-1],
306 'type': 'comment-added'}
307 self.data['submitRecords'] = self.getSubmitRecords()
308 return json.loads(json.dumps(event))
309
310 def getSubmitRecords(self):
311 status = {}
312 for cat in self.categories.keys():
313 status[cat] = 0
314
315 for a in self.patchsets[-1]['approvals']:
316 cur = status[a['type']]
317 cat_min, cat_max = self.categories[a['type']][1:]
318 new = int(a['value'])
319 if new == cat_min:
320 cur = new
321 elif abs(new) > abs(cur):
322 cur = new
323 status[a['type']] = cur
324
325 labels = []
326 ok = True
327 for typ, cat in self.categories.items():
328 cur = status[typ]
329 cat_min, cat_max = cat[1:]
330 if cur == cat_min:
331 value = 'REJECT'
332 ok = False
333 elif cur == cat_max:
334 value = 'OK'
335 else:
336 value = 'NEED'
337 ok = False
338 labels.append({'label': cat[0], 'status': value})
339 if ok:
340 return [{'status': 'OK'}]
341 return [{'status': 'NOT_READY',
342 'labels': labels}]
343
344 def setDependsOn(self, other, patchset):
345 self.depends_on_change = other
346 d = {'id': other.data['id'],
347 'number': other.data['number'],
348 'ref': other.patchsets[patchset - 1]['ref']
349 }
350 self.data['dependsOn'] = [d]
351
352 other.needed_by_changes.append(self)
353 needed = other.data.get('neededBy', [])
354 d = {'id': self.data['id'],
355 'number': self.data['number'],
356 'ref': self.patchsets[patchset - 1]['ref'],
357 'revision': self.patchsets[patchset - 1]['revision']
358 }
359 needed.append(d)
360 other.data['neededBy'] = needed
361
362 def query(self):
363 self.queried += 1
364 d = self.data.get('dependsOn')
365 if d:
366 d = d[0]
367 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
368 d['isCurrentPatchSet'] = True
369 else:
370 d['isCurrentPatchSet'] = False
371 return json.loads(json.dumps(self.data))
372
373 def setMerged(self):
374 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000375 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700376 return
377 if self.fail_merge:
378 return
379 self.data['status'] = 'MERGED'
380 self.open = False
381
382 path = os.path.join(self.upstream_root, self.project)
383 repo = git.Repo(path)
384 repo.heads[self.branch].commit = \
385 repo.commit(self.patchsets[-1]['revision'])
386
387 def setReported(self):
388 self.reported += 1
389
390
Joshua Hesketh352264b2015-08-11 23:42:08 +1000391class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700392 """A Fake Gerrit connection for use in tests.
393
394 This subclasses
395 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
396 ability for tests to add changes to the fake Gerrit it represents.
397 """
398
Joshua Hesketh352264b2015-08-11 23:42:08 +1000399 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700400
Joshua Hesketh352264b2015-08-11 23:42:08 +1000401 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700402 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000403 super(FakeGerritConnection, self).__init__(connection_name,
404 connection_config)
405
James E. Blair7fc8daa2016-08-08 15:37:15 -0700406 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700407 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
408 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000409 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700410 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200411 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700412
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700413 def addFakeChange(self, project, branch, subject, status='NEW',
414 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700415 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700416 self.change_number += 1
417 c = FakeChange(self, self.change_number, project, branch, subject,
418 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700419 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700420 self.changes[self.change_number] = c
421 return c
422
Clark Boylanb640e052014-04-03 16:41:46 -0700423 def review(self, project, changeid, message, action):
424 number, ps = changeid.split(',')
425 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000426
427 # Add the approval back onto the change (ie simulate what gerrit would
428 # do).
429 # Usually when zuul leaves a review it'll create a feedback loop where
430 # zuul's review enters another gerrit event (which is then picked up by
431 # zuul). However, we can't mimic this behaviour (by adding this
432 # approval event into the queue) as it stops jobs from checking what
433 # happens before this event is triggered. If a job needs to see what
434 # happens they can add their own verified event into the queue.
435 # Nevertheless, we can update change with the new review in gerrit.
436
James E. Blair8b5408c2016-08-08 15:37:46 -0700437 for cat in action.keys():
438 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000439 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000440
James E. Blair8b5408c2016-08-08 15:37:46 -0700441 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000442 if 'label' in action:
443 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000444 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000445
Clark Boylanb640e052014-04-03 16:41:46 -0700446 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000447
Clark Boylanb640e052014-04-03 16:41:46 -0700448 if 'submit' in action:
449 change.setMerged()
450 if message:
451 change.setReported()
452
453 def query(self, number):
454 change = self.changes.get(int(number))
455 if change:
456 return change.query()
457 return {}
458
James E. Blairc494d542014-08-06 09:23:52 -0700459 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700460 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700461 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800462 if query.startswith('change:'):
463 # Query a specific changeid
464 changeid = query[len('change:'):]
465 l = [change.query() for change in self.changes.values()
466 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700467 elif query.startswith('message:'):
468 # Query the content of a commit message
469 msg = query[len('message:'):].strip()
470 l = [change.query() for change in self.changes.values()
471 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800472 else:
473 # Query all open changes
474 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700475 return l
James E. Blairc494d542014-08-06 09:23:52 -0700476
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700478 pass
479
Joshua Hesketh352264b2015-08-11 23:42:08 +1000480 def getGitUrl(self, project):
481 return os.path.join(self.upstream_root, project.name)
482
Adam Gandelmanc5e4f1d2016-11-29 14:27:17 -0800483 def _getGitwebUrl(self, project, sha=None):
484 return self.getGitwebUrl(project, sha)
485
Clark Boylanb640e052014-04-03 16:41:46 -0700486
487class BuildHistory(object):
488 def __init__(self, **kw):
489 self.__dict__.update(kw)
490
491 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700492 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
493 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700494
495
496class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200497 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700498 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700499 self.url = url
500
501 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700502 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700503 path = res.path
504 project = '/'.join(path.split('/')[2:-2])
505 ret = '001e# service=git-upload-pack\n'
506 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
507 'multi_ack thin-pack side-band side-band-64k ofs-delta '
508 'shallow no-progress include-tag multi_ack_detailed no-done\n')
509 path = os.path.join(self.upstream_root, project)
510 repo = git.Repo(path)
511 for ref in repo.refs:
512 r = ref.object.hexsha + ' ' + ref.path + '\n'
513 ret += '%04x%s' % (len(r) + 4, r)
514 ret += '0000'
515 return ret
516
517
Clark Boylanb640e052014-04-03 16:41:46 -0700518class FakeStatsd(threading.Thread):
519 def __init__(self):
520 threading.Thread.__init__(self)
521 self.daemon = True
522 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
523 self.sock.bind(('', 0))
524 self.port = self.sock.getsockname()[1]
525 self.wake_read, self.wake_write = os.pipe()
526 self.stats = []
527
528 def run(self):
529 while True:
530 poll = select.poll()
531 poll.register(self.sock, select.POLLIN)
532 poll.register(self.wake_read, select.POLLIN)
533 ret = poll.poll()
534 for (fd, event) in ret:
535 if fd == self.sock.fileno():
536 data = self.sock.recvfrom(1024)
537 if not data:
538 return
539 self.stats.append(data[0])
540 if fd == self.wake_read:
541 return
542
543 def stop(self):
544 os.write(self.wake_write, '1\n')
545
546
James E. Blaire1767bc2016-08-02 10:00:27 -0700547class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700548 log = logging.getLogger("zuul.test")
549
James E. Blair34776ee2016-08-25 13:53:54 -0700550 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700551 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700552 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700553 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700554 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700555 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700556 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700557 # TODOv3(jeblair): self.node is really "the image of the node
558 # assigned". We should rename it (self.node_image?) if we
559 # keep using it like this, or we may end up exposing more of
560 # the complexity around multi-node jobs here
561 # (self.nodes[0].image?)
562 self.node = None
563 if len(self.parameters.get('nodes')) == 1:
564 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700565 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700566 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700567 self.wait_condition = threading.Condition()
568 self.waiting = False
569 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500570 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700571 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700572 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700573 self.changes = None
574 if 'ZUUL_CHANGE_IDS' in self.parameters:
575 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700576
James E. Blair3158e282016-08-19 09:34:11 -0700577 def __repr__(self):
578 waiting = ''
579 if self.waiting:
580 waiting = ' [waiting]'
581 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
582
Clark Boylanb640e052014-04-03 16:41:46 -0700583 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700584 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700585 self.wait_condition.acquire()
586 self.wait_condition.notify()
587 self.waiting = False
588 self.log.debug("Build %s released" % self.unique)
589 self.wait_condition.release()
590
591 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700592 """Return whether this build is being held.
593
594 :returns: Whether the build is being held.
595 :rtype: bool
596 """
597
Clark Boylanb640e052014-04-03 16:41:46 -0700598 self.wait_condition.acquire()
599 if self.waiting:
600 ret = True
601 else:
602 ret = False
603 self.wait_condition.release()
604 return ret
605
606 def _wait(self):
607 self.wait_condition.acquire()
608 self.waiting = True
609 self.log.debug("Build %s waiting" % self.unique)
610 self.wait_condition.wait()
611 self.wait_condition.release()
612
613 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700614 self.log.debug('Running build %s' % self.unique)
615
James E. Blaire1767bc2016-08-02 10:00:27 -0700616 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700617 self.log.debug('Holding build %s' % self.unique)
618 self._wait()
619 self.log.debug("Build %s continuing" % self.unique)
620
Clark Boylanb640e052014-04-03 16:41:46 -0700621 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700622 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700623 result = 'FAILURE'
624 if self.aborted:
625 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500626 if self.requeue:
627 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700628
629 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700630 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700631
James E. Blaire1767bc2016-08-02 10:00:27 -0700632 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700633
James E. Blaira5dba232016-08-08 15:53:24 -0700634 def shouldFail(self):
635 changes = self.launch_server.fail_tests.get(self.name, [])
636 for change in changes:
637 if self.hasChanges(change):
638 return True
639 return False
640
James E. Blaire7b99a02016-08-05 14:27:34 -0700641 def hasChanges(self, *changes):
642 """Return whether this build has certain changes in its git repos.
643
644 :arg FakeChange changes: One or more changes (varargs) that
645 are expected to be present (in order) in the git repository of
646 the active project.
647
648 :returns: Whether the build has the indicated changes.
649 :rtype: bool
650
651 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800652 for change in changes:
653 path = os.path.join(self.jobdir.git_root, change.project)
654 try:
655 repo = git.Repo(path)
656 except NoSuchPathError as e:
657 self.log.debug('%s' % e)
658 return False
659 ref = self.parameters['ZUUL_REF']
660 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
661 commit_message = '%s-1' % change.subject
662 self.log.debug("Checking if build %s has changes; commit_message "
663 "%s; repo_messages %s" % (self, commit_message,
664 repo_messages))
665 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700666 self.log.debug(" messages do not match")
667 return False
668 self.log.debug(" OK")
669 return True
670
Clark Boylanb640e052014-04-03 16:41:46 -0700671
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000672class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700673 """An Ansible launcher to be used in tests.
674
675 :ivar bool hold_jobs_in_build: If true, when jobs are launched
676 they will report that they have started but then pause until
677 released before reporting completion. This attribute may be
678 changed at any time and will take effect for subsequently
679 launched builds, but previously held builds will still need to
680 be explicitly released.
681
682 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800683 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700684 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800685 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700686 self.hold_jobs_in_build = False
687 self.lock = threading.Lock()
688 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700689 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700690 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700691 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800692
James E. Blaira5dba232016-08-08 15:53:24 -0700693 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700694 """Instruct the launcher to report matching builds as failures.
695
696 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700697 :arg Change change: The :py:class:`~tests.base.FakeChange`
698 instance which should cause the job to fail. This job
699 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700700
701 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700702 l = self.fail_tests.get(name, [])
703 l.append(change)
704 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800705
James E. Blair962220f2016-08-03 11:22:38 -0700706 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700707 """Release a held build.
708
709 :arg str regex: A regular expression which, if supplied, will
710 cause only builds with matching names to be released. If
711 not supplied, all builds will be released.
712
713 """
James E. Blair962220f2016-08-03 11:22:38 -0700714 builds = self.running_builds[:]
715 self.log.debug("Releasing build %s (%s)" % (regex,
716 len(self.running_builds)))
717 for build in builds:
718 if not regex or re.match(regex, build.name):
719 self.log.debug("Releasing build %s" %
720 (build.parameters['ZUUL_UUID']))
721 build.release()
722 else:
723 self.log.debug("Not releasing build %s" %
724 (build.parameters['ZUUL_UUID']))
725 self.log.debug("Done releasing builds %s (%s)" %
726 (regex, len(self.running_builds)))
727
James E. Blair17302972016-08-10 16:11:42 -0700728 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700729 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700730 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700731 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700732 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700733 super(RecordingLaunchServer, self).launchJob(job)
734
735 def stopJob(self, job):
736 self.log.debug("handle stop")
737 parameters = json.loads(job.arguments)
738 uuid = parameters['uuid']
739 for build in self.running_builds:
740 if build.unique == uuid:
741 build.aborted = True
742 build.release()
743 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700744
745 def runAnsible(self, jobdir, job):
746 build = self.job_builds[job.unique]
747 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700748
749 if self._run_ansible:
750 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
751 else:
752 result = build.run()
753
754 self.lock.acquire()
755 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700756 BuildHistory(name=build.name, result=result, changes=build.changes,
757 node=build.node, uuid=build.unique,
758 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700759 pipeline=build.parameters['ZUUL_PIPELINE'])
760 )
James E. Blairab7132b2016-08-05 12:36:22 -0700761 self.running_builds.remove(build)
762 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700763 self.lock.release()
Clint Byrum69e47122016-12-02 16:40:35 -0800764 if build.run_error:
765 result = None
James E. Blaire1767bc2016-08-02 10:00:27 -0700766 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800767
768
Clark Boylanb640e052014-04-03 16:41:46 -0700769class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700770 """A Gearman server for use in tests.
771
772 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
773 added to the queue but will not be distributed to workers
774 until released. This attribute may be changed at any time and
775 will take effect for subsequently enqueued jobs, but
776 previously held jobs will still need to be explicitly
777 released.
778
779 """
780
Clark Boylanb640e052014-04-03 16:41:46 -0700781 def __init__(self):
782 self.hold_jobs_in_queue = False
783 super(FakeGearmanServer, self).__init__(0)
784
785 def getJobForConnection(self, connection, peek=False):
786 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
787 for job in queue:
788 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500789 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700790 job.waiting = self.hold_jobs_in_queue
791 else:
792 job.waiting = False
793 if job.waiting:
794 continue
795 if job.name in connection.functions:
796 if not peek:
797 queue.remove(job)
798 connection.related_jobs[job.handle] = job
799 job.worker_connection = connection
800 job.running = True
801 return job
802 return None
803
804 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700805 """Release a held job.
806
807 :arg str regex: A regular expression which, if supplied, will
808 cause only jobs with matching names to be released. If
809 not supplied, all jobs will be released.
810 """
Clark Boylanb640e052014-04-03 16:41:46 -0700811 released = False
812 qlen = (len(self.high_queue) + len(self.normal_queue) +
813 len(self.low_queue))
814 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
815 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500816 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700817 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500818 parameters = json.loads(job.arguments)
819 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700820 self.log.debug("releasing queued job %s" %
821 job.unique)
822 job.waiting = False
823 released = True
824 else:
825 self.log.debug("not releasing queued job %s" %
826 job.unique)
827 if released:
828 self.wakeConnections()
829 qlen = (len(self.high_queue) + len(self.normal_queue) +
830 len(self.low_queue))
831 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
832
833
834class FakeSMTP(object):
835 log = logging.getLogger('zuul.FakeSMTP')
836
837 def __init__(self, messages, server, port):
838 self.server = server
839 self.port = port
840 self.messages = messages
841
842 def sendmail(self, from_email, to_email, msg):
843 self.log.info("Sending email from %s, to %s, with msg %s" % (
844 from_email, to_email, msg))
845
846 headers = msg.split('\n\n', 1)[0]
847 body = msg.split('\n\n', 1)[1]
848
849 self.messages.append(dict(
850 from_email=from_email,
851 to_email=to_email,
852 msg=msg,
853 headers=headers,
854 body=body,
855 ))
856
857 return True
858
859 def quit(self):
860 return True
861
862
863class FakeSwiftClientConnection(swiftclient.client.Connection):
864 def post_account(self, headers):
865 # Do nothing
866 pass
867
868 def get_auth(self):
869 # Returns endpoint and (unused) auth token
870 endpoint = os.path.join('https://storage.example.org', 'V1',
871 'AUTH_account')
872 return endpoint, ''
873
874
James E. Blairdce6cea2016-12-20 16:45:32 -0800875class FakeNodepool(object):
876 REQUEST_ROOT = '/nodepool/requests'
877
878 log = logging.getLogger("zuul.test.FakeNodepool")
879
880 def __init__(self, host, port, chroot):
881 self.client = kazoo.client.KazooClient(
882 hosts='%s:%s%s' % (host, port, chroot))
883 self.client.start()
884 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800885 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800886 self.thread = threading.Thread(target=self.run)
887 self.thread.daemon = True
888 self.thread.start()
889
890 def stop(self):
891 self._running = False
892 self.thread.join()
893 self.client.stop()
894 self.client.close()
895
896 def run(self):
897 while self._running:
898 self._run()
899 time.sleep(0.1)
900
901 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800902 if self.paused:
903 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800904 for req in self.getNodeRequests():
905 self.fulfillRequest(req)
906
907 def getNodeRequests(self):
908 try:
909 reqids = self.client.get_children(self.REQUEST_ROOT)
910 except kazoo.exceptions.NoNodeError:
911 return []
912 reqs = []
913 for oid in sorted(reqids):
914 path = self.REQUEST_ROOT + '/' + oid
915 data, stat = self.client.get(path)
916 data = json.loads(data)
917 data['_oid'] = oid
918 reqs.append(data)
919 return reqs
920
James E. Blaira38c28e2017-01-04 10:33:20 -0800921 def makeNode(self, request_id, node_type):
922 now = time.time()
923 path = '/nodepool/nodes/'
924 data = dict(type=node_type,
925 provider='test-provider',
926 region='test-region',
927 az=None,
928 public_ipv4='127.0.0.1',
929 private_ipv4=None,
930 public_ipv6=None,
931 allocated_to=request_id,
932 state='ready',
933 state_time=now,
934 created_time=now,
935 updated_time=now,
936 image_id=None,
937 launcher='fake-nodepool')
938 data = json.dumps(data)
939 path = self.client.create(path, data,
940 makepath=True,
941 sequence=True)
942 nodeid = path.split("/")[-1]
943 return nodeid
944
James E. Blairdce6cea2016-12-20 16:45:32 -0800945 def fulfillRequest(self, request):
946 if request['state'] == 'fulfilled':
947 return
948 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800949 oid = request['_oid']
950 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800951
952 nodes = []
953 for node in request['node_types']:
954 nodeid = self.makeNode(oid, node)
955 nodes.append(nodeid)
956
957 request['state'] = 'fulfilled'
958 request['state_time'] = time.time()
959 request['nodes'] = nodes
James E. Blairdce6cea2016-12-20 16:45:32 -0800960 path = self.REQUEST_ROOT + '/' + oid
961 data = json.dumps(request)
962 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
963 self.client.set(path, data)
964
965
James E. Blair498059b2016-12-20 13:50:13 -0800966class ChrootedKazooFixture(fixtures.Fixture):
967 def __init__(self):
968 super(ChrootedKazooFixture, self).__init__()
969
970 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
971 if ':' in zk_host:
972 host, port = zk_host.split(':')
973 else:
974 host = zk_host
975 port = None
976
977 self.zookeeper_host = host
978
979 if not port:
980 self.zookeeper_port = 2181
981 else:
982 self.zookeeper_port = int(port)
983
984 def _setUp(self):
985 # Make sure the test chroot paths do not conflict
986 random_bits = ''.join(random.choice(string.ascii_lowercase +
987 string.ascii_uppercase)
988 for x in range(8))
989
990 rand_test_path = '%s_%s' % (random_bits, os.getpid())
991 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
992
993 # Ensure the chroot path exists and clean up any pre-existing znodes.
994 _tmp_client = kazoo.client.KazooClient(
995 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
996 _tmp_client.start()
997
998 if _tmp_client.exists(self.zookeeper_chroot):
999 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1000
1001 _tmp_client.ensure_path(self.zookeeper_chroot)
1002 _tmp_client.stop()
1003 _tmp_client.close()
1004
1005 self.addCleanup(self._cleanup)
1006
1007 def _cleanup(self):
1008 '''Remove the chroot path.'''
1009 # Need a non-chroot'ed client to remove the chroot path
1010 _tmp_client = kazoo.client.KazooClient(
1011 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1012 _tmp_client.start()
1013 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1014 _tmp_client.stop()
1015
1016
Maru Newby3fe5f852015-01-13 04:22:14 +00001017class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001018 log = logging.getLogger("zuul.test")
1019
1020 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001021 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001022 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1023 try:
1024 test_timeout = int(test_timeout)
1025 except ValueError:
1026 # If timeout value is invalid do not set a timeout.
1027 test_timeout = 0
1028 if test_timeout > 0:
1029 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1030
1031 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1032 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1033 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1034 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1035 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1036 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1037 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1038 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1039 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1040 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -07001041 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001042 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
1043 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -07001044 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
1045 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001046 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
1047 log_level = logging.WARNING
1048 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
1049 log_level = logging.ERROR
1050 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
1051 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -07001052 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001053 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -07001054 format='%(asctime)s %(name)-32s '
1055 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +00001056
James E. Blairdce6cea2016-12-20 16:45:32 -08001057 # NOTE(notmorgan): Extract logging overrides for specific libraries
1058 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
1059 # each. This is used to limit the output during test runs from
1060 # libraries that zuul depends on such as gear.
1061 log_defaults_from_env = os.environ.get(
1062 'OS_LOG_DEFAULTS',
1063 'git.cmd=INFO,kazoo.client=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001064
James E. Blairdce6cea2016-12-20 16:45:32 -08001065 if log_defaults_from_env:
1066 for default in log_defaults_from_env.split(','):
1067 try:
1068 name, level_str = default.split('=', 1)
1069 level = getattr(logging, level_str, logging.DEBUG)
1070 self.useFixture(fixtures.FakeLogger(
1071 name=name,
1072 level=level,
1073 format='%(asctime)s %(name)-32s '
1074 '%(levelname)-8s %(message)s'))
1075 except ValueError:
1076 # NOTE(notmorgan): Invalid format of the log default,
1077 # skip and don't try and apply a logger for the
1078 # specified module
1079 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001080
Maru Newby3fe5f852015-01-13 04:22:14 +00001081
1082class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001083 """A test case with a functioning Zuul.
1084
1085 The following class variables are used during test setup and can
1086 be overidden by subclasses but are effectively read-only once a
1087 test method starts running:
1088
1089 :cvar str config_file: This points to the main zuul config file
1090 within the fixtures directory. Subclasses may override this
1091 to obtain a different behavior.
1092
1093 :cvar str tenant_config_file: This is the tenant config file
1094 (which specifies from what git repos the configuration should
1095 be loaded). It defaults to the value specified in
1096 `config_file` but can be overidden by subclasses to obtain a
1097 different tenant/project layout while using the standard main
1098 configuration.
1099
1100 The following are instance variables that are useful within test
1101 methods:
1102
1103 :ivar FakeGerritConnection fake_<connection>:
1104 A :py:class:`~tests.base.FakeGerritConnection` will be
1105 instantiated for each connection present in the config file
1106 and stored here. For instance, `fake_gerrit` will hold the
1107 FakeGerritConnection object for a connection named `gerrit`.
1108
1109 :ivar FakeGearmanServer gearman_server: An instance of
1110 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1111 server that all of the Zuul components in this test use to
1112 communicate with each other.
1113
1114 :ivar RecordingLaunchServer launch_server: An instance of
1115 :py:class:`~tests.base.RecordingLaunchServer` which is the
1116 Ansible launch server used to run jobs for this test.
1117
1118 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1119 representing currently running builds. They are appended to
1120 the list in the order they are launched, and removed from this
1121 list upon completion.
1122
1123 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1124 objects representing completed builds. They are appended to
1125 the list in the order they complete.
1126
1127 """
1128
James E. Blair83005782015-12-11 14:46:03 -08001129 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001130 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001131
1132 def _startMerger(self):
1133 self.merge_server = zuul.merger.server.MergeServer(self.config,
1134 self.connections)
1135 self.merge_server.start()
1136
Maru Newby3fe5f852015-01-13 04:22:14 +00001137 def setUp(self):
1138 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001139
1140 self.setupZK()
1141
James E. Blair97d902e2014-08-21 13:25:56 -07001142 if USE_TEMPDIR:
1143 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001144 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1145 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001146 else:
1147 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001148 self.test_root = os.path.join(tmp_root, "zuul-test")
1149 self.upstream_root = os.path.join(self.test_root, "upstream")
1150 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001151 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001152
1153 if os.path.exists(self.test_root):
1154 shutil.rmtree(self.test_root)
1155 os.makedirs(self.test_root)
1156 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001157 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001158
1159 # Make per test copy of Configuration.
1160 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001161 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001162 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001163 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001164 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001165 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001166
1167 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001168 # TODOv3(jeblair): remove these and replace with new git
1169 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001170 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001171 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001172 self.init_repo("org/project5")
1173 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001174 self.init_repo("org/one-job-project")
1175 self.init_repo("org/nonvoting-project")
1176 self.init_repo("org/templated-project")
1177 self.init_repo("org/layered-project")
1178 self.init_repo("org/node-project")
1179 self.init_repo("org/conflict-project")
1180 self.init_repo("org/noop-project")
1181 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001182 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001183
1184 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001185 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1186 # see: https://github.com/jsocol/pystatsd/issues/61
1187 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001188 os.environ['STATSD_PORT'] = str(self.statsd.port)
1189 self.statsd.start()
1190 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001191 reload_module(statsd)
1192 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001193
1194 self.gearman_server = FakeGearmanServer()
1195
1196 self.config.set('gearman', 'port', str(self.gearman_server.port))
1197
Joshua Hesketh352264b2015-08-11 23:42:08 +10001198 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1199 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1200 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001201
Joshua Hesketh352264b2015-08-11 23:42:08 +10001202 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001203
1204 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1205 FakeSwiftClientConnection))
1206 self.swift = zuul.lib.swift.Swift(self.config)
1207
Jan Hruban6b71aff2015-10-22 16:58:08 +02001208 self.event_queues = [
1209 self.sched.result_event_queue,
1210 self.sched.trigger_event_queue
1211 ]
1212
James E. Blairfef78942016-03-11 16:28:56 -08001213 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001214 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001215
Clark Boylanb640e052014-04-03 16:41:46 -07001216 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001217 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001218 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001219 return FakeURLOpener(self.upstream_root, *args, **kw)
1220
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001221 old_urlopen = urllib.request.urlopen
1222 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001223
James E. Blair3f876d52016-07-22 13:07:14 -07001224 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001225
James E. Blaire1767bc2016-08-02 10:00:27 -07001226 self.launch_server = RecordingLaunchServer(
1227 self.config, self.connections, _run_ansible=self.run_ansible)
1228 self.launch_server.start()
1229 self.history = self.launch_server.build_history
1230 self.builds = self.launch_server.running_builds
1231
1232 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001233 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001234 self.merge_client = zuul.merger.client.MergeClient(
1235 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001236 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001237 self.zk = zuul.zk.ZooKeeper()
1238 self.zk.connect([self.zk_config])
1239
1240 self.fake_nodepool = FakeNodepool(self.zk_config.host,
1241 self.zk_config.port,
1242 self.zk_config.chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001243
James E. Blaire1767bc2016-08-02 10:00:27 -07001244 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001245 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001246 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001247 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001248
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001249 self.webapp = zuul.webapp.WebApp(
1250 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001251 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001252
1253 self.sched.start()
1254 self.sched.reconfigure(self.config)
1255 self.sched.resume()
1256 self.webapp.start()
1257 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001258 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001259
1260 self.addCleanup(self.assertFinalState)
1261 self.addCleanup(self.shutdown)
1262
James E. Blairfef78942016-03-11 16:28:56 -08001263 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001264 # Register connections from the config
1265 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001266
Joshua Hesketh352264b2015-08-11 23:42:08 +10001267 def FakeSMTPFactory(*args, **kw):
1268 args = [self.smtp_messages] + list(args)
1269 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001270
Joshua Hesketh352264b2015-08-11 23:42:08 +10001271 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001272
Joshua Hesketh352264b2015-08-11 23:42:08 +10001273 # Set a changes database so multiple FakeGerrit's can report back to
1274 # a virtual canonical database given by the configured hostname
1275 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001276 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001277
Joshua Hesketh352264b2015-08-11 23:42:08 +10001278 for section_name in self.config.sections():
1279 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1280 section_name, re.I)
1281 if not con_match:
1282 continue
1283 con_name = con_match.group(2)
1284 con_config = dict(self.config.items(section_name))
1285
1286 if 'driver' not in con_config:
1287 raise Exception("No driver specified for connection %s."
1288 % con_name)
1289
1290 con_driver = con_config['driver']
1291
1292 # TODO(jhesketh): load the required class automatically
1293 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001294 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1295 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001296 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001297 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001298 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001299 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001300 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001301 self.event_queues.append(
1302 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001303 setattr(self, 'fake_' + con_name,
1304 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001305 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001306 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001307 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1308 else:
1309 raise Exception("Unknown driver, %s, for connection %s"
1310 % (con_config['driver'], con_name))
1311
1312 # If the [gerrit] or [smtp] sections still exist, load them in as a
1313 # connection named 'gerrit' or 'smtp' respectfully
1314
1315 if 'gerrit' in self.config.sections():
1316 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001317 self.event_queues.append(
1318 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001319 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001320 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001321 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001322
1323 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001324 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001325 zuul.connection.smtp.SMTPConnection(
1326 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001327
James E. Blair83005782015-12-11 14:46:03 -08001328 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001329 # This creates the per-test configuration object. It can be
1330 # overriden by subclasses, but should not need to be since it
1331 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001332 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001333 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001334 if hasattr(self, 'tenant_config_file'):
1335 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001336 git_path = os.path.join(
1337 os.path.dirname(
1338 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1339 'git')
1340 if os.path.exists(git_path):
1341 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001342 project = reponame.replace('_', '/')
1343 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001344 os.path.join(git_path, reponame))
1345
James E. Blair498059b2016-12-20 13:50:13 -08001346 def setupZK(self):
1347 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blairdce6cea2016-12-20 16:45:32 -08001348 self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
1349 self.zk_chroot_fixture.zookeeper_host,
1350 self.zk_chroot_fixture.zookeeper_port,
1351 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001352
James E. Blair96c6bf82016-01-15 16:20:40 -08001353 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001354 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001355
1356 files = {}
1357 for (dirpath, dirnames, filenames) in os.walk(source_path):
1358 for filename in filenames:
1359 test_tree_filepath = os.path.join(dirpath, filename)
1360 common_path = os.path.commonprefix([test_tree_filepath,
1361 source_path])
1362 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1363 with open(test_tree_filepath, 'r') as f:
1364 content = f.read()
1365 files[relative_filepath] = content
1366 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001367 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001368
Clark Boylanb640e052014-04-03 16:41:46 -07001369 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001370 # Make sure that git.Repo objects have been garbage collected.
1371 repos = []
1372 gc.collect()
1373 for obj in gc.get_objects():
1374 if isinstance(obj, git.Repo):
1375 repos.append(obj)
1376 self.assertEqual(len(repos), 0)
1377 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001378 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001379 for tenant in self.sched.abide.tenants.values():
1380 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001381 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001382 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001383
1384 def shutdown(self):
1385 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001386 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001387 self.merge_server.stop()
1388 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001389 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001390 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001391 self.sched.stop()
1392 self.sched.join()
1393 self.statsd.stop()
1394 self.statsd.join()
1395 self.webapp.stop()
1396 self.webapp.join()
1397 self.rpc.stop()
1398 self.rpc.join()
1399 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001400 self.fake_nodepool.stop()
1401 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001402 threads = threading.enumerate()
1403 if len(threads) > 1:
1404 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001405 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001406
1407 def init_repo(self, project):
1408 parts = project.split('/')
1409 path = os.path.join(self.upstream_root, *parts[:-1])
1410 if not os.path.exists(path):
1411 os.makedirs(path)
1412 path = os.path.join(self.upstream_root, project)
1413 repo = git.Repo.init(path)
1414
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001415 with repo.config_writer() as config_writer:
1416 config_writer.set_value('user', 'email', 'user@example.com')
1417 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001418
Clark Boylanb640e052014-04-03 16:41:46 -07001419 repo.index.commit('initial commit')
1420 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001421
James E. Blair97d902e2014-08-21 13:25:56 -07001422 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001423 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001424 repo.git.clean('-x', '-f', '-d')
1425
James E. Blair97d902e2014-08-21 13:25:56 -07001426 def create_branch(self, project, branch):
1427 path = os.path.join(self.upstream_root, project)
1428 repo = git.Repo.init(path)
1429 fn = os.path.join(path, 'README')
1430
1431 branch_head = repo.create_head(branch)
1432 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001433 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001434 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001435 f.close()
1436 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001437 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001438
James E. Blair97d902e2014-08-21 13:25:56 -07001439 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001440 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001441 repo.git.clean('-x', '-f', '-d')
1442
Sachi King9f16d522016-03-16 12:20:45 +11001443 def create_commit(self, project):
1444 path = os.path.join(self.upstream_root, project)
1445 repo = git.Repo(path)
1446 repo.head.reference = repo.heads['master']
1447 file_name = os.path.join(path, 'README')
1448 with open(file_name, 'a') as f:
1449 f.write('creating fake commit\n')
1450 repo.index.add([file_name])
1451 commit = repo.index.commit('Creating a fake commit')
1452 return commit.hexsha
1453
James E. Blairb8c16472015-05-05 14:55:26 -07001454 def orderedRelease(self):
1455 # Run one build at a time to ensure non-race order:
1456 while len(self.builds):
1457 self.release(self.builds[0])
1458 self.waitUntilSettled()
1459
Clark Boylanb640e052014-04-03 16:41:46 -07001460 def release(self, job):
1461 if isinstance(job, FakeBuild):
1462 job.release()
1463 else:
1464 job.waiting = False
1465 self.log.debug("Queued job %s released" % job.unique)
1466 self.gearman_server.wakeConnections()
1467
1468 def getParameter(self, job, name):
1469 if isinstance(job, FakeBuild):
1470 return job.parameters[name]
1471 else:
1472 parameters = json.loads(job.arguments)
1473 return parameters[name]
1474
Clark Boylanb640e052014-04-03 16:41:46 -07001475 def haveAllBuildsReported(self):
1476 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001477 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001478 return False
1479 # Find out if every build that the worker has completed has been
1480 # reported back to Zuul. If it hasn't then that means a Gearman
1481 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001482 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001483 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001484 if not zbuild:
1485 # It has already been reported
1486 continue
1487 # It hasn't been reported yet.
1488 return False
1489 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001490 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001491 if connection.state == 'GRAB_WAIT':
1492 return False
1493 return True
1494
1495 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001496 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001497 for build in builds:
1498 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001499 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001500 for j in conn.related_jobs.values():
1501 if j.unique == build.uuid:
1502 client_job = j
1503 break
1504 if not client_job:
1505 self.log.debug("%s is not known to the gearman client" %
1506 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001507 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001508 if not client_job.handle:
1509 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001510 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001511 server_job = self.gearman_server.jobs.get(client_job.handle)
1512 if not server_job:
1513 self.log.debug("%s is not known to the gearman server" %
1514 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001515 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001516 if not hasattr(server_job, 'waiting'):
1517 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001518 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001519 if server_job.waiting:
1520 continue
James E. Blair17302972016-08-10 16:11:42 -07001521 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001522 self.log.debug("%s has not reported start" % build)
1523 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001524 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001525 if worker_build:
1526 if worker_build.isWaiting():
1527 continue
1528 else:
1529 self.log.debug("%s is running" % worker_build)
1530 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001531 else:
James E. Blair962220f2016-08-03 11:22:38 -07001532 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001533 return False
1534 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001535
James E. Blairdce6cea2016-12-20 16:45:32 -08001536 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001537 if self.fake_nodepool.paused:
1538 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001539 if self.sched.nodepool.requests:
1540 return False
1541 return True
1542
Jan Hruban6b71aff2015-10-22 16:58:08 +02001543 def eventQueuesEmpty(self):
1544 for queue in self.event_queues:
1545 yield queue.empty()
1546
1547 def eventQueuesJoin(self):
1548 for queue in self.event_queues:
1549 queue.join()
1550
Clark Boylanb640e052014-04-03 16:41:46 -07001551 def waitUntilSettled(self):
1552 self.log.debug("Waiting until settled...")
1553 start = time.time()
1554 while True:
1555 if time.time() - start > 10:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001556 self.log.error("Timeout waiting for Zuul to settle")
1557 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001558 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001559 self.log.error(" %s: %s" % (queue, queue.empty()))
1560 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001561 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001562 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001563 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001564 self.log.error("All requests completed: %s" %
1565 (self.areAllNodeRequestsComplete(),))
1566 self.log.error("Merge client jobs: %s" %
1567 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001568 raise Exception("Timeout waiting for Zuul to settle")
1569 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001570
James E. Blaire1767bc2016-08-02 10:00:27 -07001571 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001572 # have all build states propogated to zuul?
1573 if self.haveAllBuildsReported():
1574 # Join ensures that the queue is empty _and_ events have been
1575 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001576 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001577 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001578 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001579 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001580 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001581 self.areAllBuildsWaiting() and
1582 self.areAllNodeRequestsComplete()):
Clark Boylanb640e052014-04-03 16:41:46 -07001583 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001584 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001585 self.log.debug("...settled.")
1586 return
1587 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001588 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001589 self.sched.wake_event.wait(0.1)
1590
1591 def countJobResults(self, jobs, result):
1592 jobs = filter(lambda x: x.result == result, jobs)
1593 return len(jobs)
1594
James E. Blair96c6bf82016-01-15 16:20:40 -08001595 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001596 for job in self.history:
1597 if (job.name == name and
1598 (project is None or
1599 job.parameters['ZUUL_PROJECT'] == project)):
1600 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001601 raise Exception("Unable to find job %s in history" % name)
1602
1603 def assertEmptyQueues(self):
1604 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001605 for tenant in self.sched.abide.tenants.values():
1606 for pipeline in tenant.layout.pipelines.values():
1607 for queue in pipeline.queues:
1608 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001609 print('pipeline %s queue %s contents %s' % (
1610 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001611 self.assertEqual(len(queue.queue), 0,
1612 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001613
1614 def assertReportedStat(self, key, value=None, kind=None):
1615 start = time.time()
1616 while time.time() < (start + 5):
1617 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001618 k, v = stat.split(':')
1619 if key == k:
1620 if value is None and kind is None:
1621 return
1622 elif value:
1623 if value == v:
1624 return
1625 elif kind:
1626 if v.endswith('|' + kind):
1627 return
1628 time.sleep(0.1)
1629
Clark Boylanb640e052014-04-03 16:41:46 -07001630 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001631
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001632 def assertBuilds(self, builds):
1633 """Assert that the running builds are as described.
1634
1635 The list of running builds is examined and must match exactly
1636 the list of builds described by the input.
1637
1638 :arg list builds: A list of dictionaries. Each item in the
1639 list must match the corresponding build in the build
1640 history, and each element of the dictionary must match the
1641 corresponding attribute of the build.
1642
1643 """
James E. Blair3158e282016-08-19 09:34:11 -07001644 try:
1645 self.assertEqual(len(self.builds), len(builds))
1646 for i, d in enumerate(builds):
1647 for k, v in d.items():
1648 self.assertEqual(
1649 getattr(self.builds[i], k), v,
1650 "Element %i in builds does not match" % (i,))
1651 except Exception:
1652 for build in self.builds:
1653 self.log.error("Running build: %s" % build)
1654 else:
1655 self.log.error("No running builds")
1656 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001657
James E. Blairb536ecc2016-08-31 10:11:42 -07001658 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001659 """Assert that the completed builds are as described.
1660
1661 The list of completed builds is examined and must match
1662 exactly the list of builds described by the input.
1663
1664 :arg list history: A list of dictionaries. Each item in the
1665 list must match the corresponding build in the build
1666 history, and each element of the dictionary must match the
1667 corresponding attribute of the build.
1668
James E. Blairb536ecc2016-08-31 10:11:42 -07001669 :arg bool ordered: If true, the history must match the order
1670 supplied, if false, the builds are permitted to have
1671 arrived in any order.
1672
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001673 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001674 def matches(history_item, item):
1675 for k, v in item.items():
1676 if getattr(history_item, k) != v:
1677 return False
1678 return True
James E. Blair3158e282016-08-19 09:34:11 -07001679 try:
1680 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001681 if ordered:
1682 for i, d in enumerate(history):
1683 if not matches(self.history[i], d):
1684 raise Exception(
1685 "Element %i in history does not match" % (i,))
1686 else:
1687 unseen = self.history[:]
1688 for i, d in enumerate(history):
1689 found = False
1690 for unseen_item in unseen:
1691 if matches(unseen_item, d):
1692 found = True
1693 unseen.remove(unseen_item)
1694 break
1695 if not found:
1696 raise Exception("No match found for element %i "
1697 "in history" % (i,))
1698 if unseen:
1699 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001700 except Exception:
1701 for build in self.history:
1702 self.log.error("Completed build: %s" % build)
1703 else:
1704 self.log.error("No completed builds")
1705 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001706
James E. Blair6ac368c2016-12-22 18:07:20 -08001707 def printHistory(self):
1708 """Log the build history.
1709
1710 This can be useful during tests to summarize what jobs have
1711 completed.
1712
1713 """
1714 self.log.debug("Build history:")
1715 for build in self.history:
1716 self.log.debug(build)
1717
James E. Blair59fdbac2015-12-07 17:08:06 -08001718 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001719 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1720
1721 def updateConfigLayout(self, path):
1722 root = os.path.join(self.test_root, "config")
1723 os.makedirs(root)
1724 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1725 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001726- tenant:
1727 name: openstack
1728 source:
1729 gerrit:
1730 config-repos:
1731 - %s
1732 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001733 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001734 self.config.set('zuul', 'tenant_config',
1735 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001736
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001737 def addCommitToRepo(self, project, message, files,
1738 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001739 path = os.path.join(self.upstream_root, project)
1740 repo = git.Repo(path)
1741 repo.head.reference = branch
1742 zuul.merger.merger.reset_repo_to_head(repo)
1743 for fn, content in files.items():
1744 fn = os.path.join(path, fn)
1745 with open(fn, 'w') as f:
1746 f.write(content)
1747 repo.index.add([fn])
1748 commit = repo.index.commit(message)
1749 repo.heads[branch].commit = commit
1750 repo.head.reference = branch
1751 repo.git.clean('-x', '-f', '-d')
1752 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001753 if tag:
1754 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001755
James E. Blair7fc8daa2016-08-08 15:37:15 -07001756 def addEvent(self, connection, event):
1757 """Inject a Fake (Gerrit) event.
1758
1759 This method accepts a JSON-encoded event and simulates Zuul
1760 having received it from Gerrit. It could (and should)
1761 eventually apply to any connection type, but is currently only
1762 used with Gerrit connections. The name of the connection is
1763 used to look up the corresponding server, and the event is
1764 simulated as having been received by all Zuul connections
1765 attached to that server. So if two Gerrit connections in Zuul
1766 are connected to the same Gerrit server, and you invoke this
1767 method specifying the name of one of them, the event will be
1768 received by both.
1769
1770 .. note::
1771
1772 "self.fake_gerrit.addEvent" calls should be migrated to
1773 this method.
1774
1775 :arg str connection: The name of the connection corresponding
1776 to the gerrit server.
1777 :arg str event: The JSON-encoded event.
1778
1779 """
1780 specified_conn = self.connections.connections[connection]
1781 for conn in self.connections.connections.values():
1782 if (isinstance(conn, specified_conn.__class__) and
1783 specified_conn.server == conn.server):
1784 conn.addEvent(event)
1785
James E. Blair3f876d52016-07-22 13:07:14 -07001786
1787class AnsibleZuulTestCase(ZuulTestCase):
1788 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001789 run_ansible = True