blob: 56c83f21cf44ca4a9a6af093f6c7911eb8f69e86 [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'
James E. Blaire18d4602017-01-05 11:17:28 -0800877 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800878
879 log = logging.getLogger("zuul.test.FakeNodepool")
880
881 def __init__(self, host, port, chroot):
882 self.client = kazoo.client.KazooClient(
883 hosts='%s:%s%s' % (host, port, chroot))
884 self.client.start()
885 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800886 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800887 self.thread = threading.Thread(target=self.run)
888 self.thread.daemon = True
889 self.thread.start()
890
891 def stop(self):
892 self._running = False
893 self.thread.join()
894 self.client.stop()
895 self.client.close()
896
897 def run(self):
898 while self._running:
899 self._run()
900 time.sleep(0.1)
901
902 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800903 if self.paused:
904 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800905 for req in self.getNodeRequests():
906 self.fulfillRequest(req)
907
908 def getNodeRequests(self):
909 try:
910 reqids = self.client.get_children(self.REQUEST_ROOT)
911 except kazoo.exceptions.NoNodeError:
912 return []
913 reqs = []
914 for oid in sorted(reqids):
915 path = self.REQUEST_ROOT + '/' + oid
916 data, stat = self.client.get(path)
917 data = json.loads(data)
918 data['_oid'] = oid
919 reqs.append(data)
920 return reqs
921
James E. Blaire18d4602017-01-05 11:17:28 -0800922 def getNodes(self):
923 try:
924 nodeids = self.client.get_children(self.NODE_ROOT)
925 except kazoo.exceptions.NoNodeError:
926 return []
927 nodes = []
928 for oid in sorted(nodeids):
929 path = self.NODE_ROOT + '/' + oid
930 data, stat = self.client.get(path)
931 data = json.loads(data)
932 data['_oid'] = oid
933 try:
934 lockfiles = self.client.get_children(path + '/lock')
935 except kazoo.exceptions.NoNodeError:
936 lockfiles = []
937 if lockfiles:
938 data['_lock'] = True
939 else:
940 data['_lock'] = False
941 nodes.append(data)
942 return nodes
943
James E. Blaira38c28e2017-01-04 10:33:20 -0800944 def makeNode(self, request_id, node_type):
945 now = time.time()
946 path = '/nodepool/nodes/'
947 data = dict(type=node_type,
948 provider='test-provider',
949 region='test-region',
950 az=None,
951 public_ipv4='127.0.0.1',
952 private_ipv4=None,
953 public_ipv6=None,
954 allocated_to=request_id,
955 state='ready',
956 state_time=now,
957 created_time=now,
958 updated_time=now,
959 image_id=None,
960 launcher='fake-nodepool')
961 data = json.dumps(data)
962 path = self.client.create(path, data,
963 makepath=True,
964 sequence=True)
965 nodeid = path.split("/")[-1]
966 return nodeid
967
James E. Blairdce6cea2016-12-20 16:45:32 -0800968 def fulfillRequest(self, request):
969 if request['state'] == 'fulfilled':
970 return
971 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800972 oid = request['_oid']
973 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800974
975 nodes = []
976 for node in request['node_types']:
977 nodeid = self.makeNode(oid, node)
978 nodes.append(nodeid)
979
980 request['state'] = 'fulfilled'
981 request['state_time'] = time.time()
982 request['nodes'] = nodes
James E. Blairdce6cea2016-12-20 16:45:32 -0800983 path = self.REQUEST_ROOT + '/' + oid
984 data = json.dumps(request)
985 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
986 self.client.set(path, data)
987
988
James E. Blair498059b2016-12-20 13:50:13 -0800989class ChrootedKazooFixture(fixtures.Fixture):
990 def __init__(self):
991 super(ChrootedKazooFixture, self).__init__()
992
993 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
994 if ':' in zk_host:
995 host, port = zk_host.split(':')
996 else:
997 host = zk_host
998 port = None
999
1000 self.zookeeper_host = host
1001
1002 if not port:
1003 self.zookeeper_port = 2181
1004 else:
1005 self.zookeeper_port = int(port)
1006
1007 def _setUp(self):
1008 # Make sure the test chroot paths do not conflict
1009 random_bits = ''.join(random.choice(string.ascii_lowercase +
1010 string.ascii_uppercase)
1011 for x in range(8))
1012
1013 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1014 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1015
1016 # Ensure the chroot path exists and clean up any pre-existing znodes.
1017 _tmp_client = kazoo.client.KazooClient(
1018 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1019 _tmp_client.start()
1020
1021 if _tmp_client.exists(self.zookeeper_chroot):
1022 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1023
1024 _tmp_client.ensure_path(self.zookeeper_chroot)
1025 _tmp_client.stop()
1026 _tmp_client.close()
1027
1028 self.addCleanup(self._cleanup)
1029
1030 def _cleanup(self):
1031 '''Remove the chroot path.'''
1032 # Need a non-chroot'ed client to remove the chroot path
1033 _tmp_client = kazoo.client.KazooClient(
1034 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1035 _tmp_client.start()
1036 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1037 _tmp_client.stop()
1038
1039
Maru Newby3fe5f852015-01-13 04:22:14 +00001040class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001041 log = logging.getLogger("zuul.test")
1042
1043 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001044 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001045 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1046 try:
1047 test_timeout = int(test_timeout)
1048 except ValueError:
1049 # If timeout value is invalid do not set a timeout.
1050 test_timeout = 0
1051 if test_timeout > 0:
1052 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1053
1054 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1055 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1056 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1057 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1058 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1059 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1060 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1061 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1062 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1063 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -07001064 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001065 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
1066 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -07001067 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
1068 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001069 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
1070 log_level = logging.WARNING
1071 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
1072 log_level = logging.ERROR
1073 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
1074 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -07001075 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001076 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -07001077 format='%(asctime)s %(name)-32s '
1078 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +00001079
James E. Blairdce6cea2016-12-20 16:45:32 -08001080 # NOTE(notmorgan): Extract logging overrides for specific libraries
1081 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
1082 # each. This is used to limit the output during test runs from
1083 # libraries that zuul depends on such as gear.
1084 log_defaults_from_env = os.environ.get(
1085 'OS_LOG_DEFAULTS',
1086 'git.cmd=INFO,kazoo.client=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001087
James E. Blairdce6cea2016-12-20 16:45:32 -08001088 if log_defaults_from_env:
1089 for default in log_defaults_from_env.split(','):
1090 try:
1091 name, level_str = default.split('=', 1)
1092 level = getattr(logging, level_str, logging.DEBUG)
1093 self.useFixture(fixtures.FakeLogger(
1094 name=name,
1095 level=level,
1096 format='%(asctime)s %(name)-32s '
1097 '%(levelname)-8s %(message)s'))
1098 except ValueError:
1099 # NOTE(notmorgan): Invalid format of the log default,
1100 # skip and don't try and apply a logger for the
1101 # specified module
1102 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001103
Maru Newby3fe5f852015-01-13 04:22:14 +00001104
1105class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001106 """A test case with a functioning Zuul.
1107
1108 The following class variables are used during test setup and can
1109 be overidden by subclasses but are effectively read-only once a
1110 test method starts running:
1111
1112 :cvar str config_file: This points to the main zuul config file
1113 within the fixtures directory. Subclasses may override this
1114 to obtain a different behavior.
1115
1116 :cvar str tenant_config_file: This is the tenant config file
1117 (which specifies from what git repos the configuration should
1118 be loaded). It defaults to the value specified in
1119 `config_file` but can be overidden by subclasses to obtain a
1120 different tenant/project layout while using the standard main
1121 configuration.
1122
1123 The following are instance variables that are useful within test
1124 methods:
1125
1126 :ivar FakeGerritConnection fake_<connection>:
1127 A :py:class:`~tests.base.FakeGerritConnection` will be
1128 instantiated for each connection present in the config file
1129 and stored here. For instance, `fake_gerrit` will hold the
1130 FakeGerritConnection object for a connection named `gerrit`.
1131
1132 :ivar FakeGearmanServer gearman_server: An instance of
1133 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1134 server that all of the Zuul components in this test use to
1135 communicate with each other.
1136
1137 :ivar RecordingLaunchServer launch_server: An instance of
1138 :py:class:`~tests.base.RecordingLaunchServer` which is the
1139 Ansible launch server used to run jobs for this test.
1140
1141 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1142 representing currently running builds. They are appended to
1143 the list in the order they are launched, and removed from this
1144 list upon completion.
1145
1146 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1147 objects representing completed builds. They are appended to
1148 the list in the order they complete.
1149
1150 """
1151
James E. Blair83005782015-12-11 14:46:03 -08001152 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001153 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001154
1155 def _startMerger(self):
1156 self.merge_server = zuul.merger.server.MergeServer(self.config,
1157 self.connections)
1158 self.merge_server.start()
1159
Maru Newby3fe5f852015-01-13 04:22:14 +00001160 def setUp(self):
1161 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001162
1163 self.setupZK()
1164
James E. Blair97d902e2014-08-21 13:25:56 -07001165 if USE_TEMPDIR:
1166 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001167 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1168 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001169 else:
1170 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001171 self.test_root = os.path.join(tmp_root, "zuul-test")
1172 self.upstream_root = os.path.join(self.test_root, "upstream")
1173 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001174 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001175
1176 if os.path.exists(self.test_root):
1177 shutil.rmtree(self.test_root)
1178 os.makedirs(self.test_root)
1179 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001180 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001181
1182 # Make per test copy of Configuration.
1183 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001184 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001185 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001186 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001187 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001188 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001189
1190 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001191 # TODOv3(jeblair): remove these and replace with new git
1192 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001193 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001194 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001195 self.init_repo("org/project5")
1196 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001197 self.init_repo("org/one-job-project")
1198 self.init_repo("org/nonvoting-project")
1199 self.init_repo("org/templated-project")
1200 self.init_repo("org/layered-project")
1201 self.init_repo("org/node-project")
1202 self.init_repo("org/conflict-project")
1203 self.init_repo("org/noop-project")
1204 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001205 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001206
1207 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001208 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1209 # see: https://github.com/jsocol/pystatsd/issues/61
1210 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001211 os.environ['STATSD_PORT'] = str(self.statsd.port)
1212 self.statsd.start()
1213 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001214 reload_module(statsd)
1215 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001216
1217 self.gearman_server = FakeGearmanServer()
1218
1219 self.config.set('gearman', 'port', str(self.gearman_server.port))
1220
Joshua Hesketh352264b2015-08-11 23:42:08 +10001221 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1222 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1223 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001224
Joshua Hesketh352264b2015-08-11 23:42:08 +10001225 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001226
1227 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1228 FakeSwiftClientConnection))
1229 self.swift = zuul.lib.swift.Swift(self.config)
1230
Jan Hruban6b71aff2015-10-22 16:58:08 +02001231 self.event_queues = [
1232 self.sched.result_event_queue,
1233 self.sched.trigger_event_queue
1234 ]
1235
James E. Blairfef78942016-03-11 16:28:56 -08001236 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001237 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001238
Clark Boylanb640e052014-04-03 16:41:46 -07001239 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001240 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001241 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001242 return FakeURLOpener(self.upstream_root, *args, **kw)
1243
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001244 old_urlopen = urllib.request.urlopen
1245 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001246
James E. Blair3f876d52016-07-22 13:07:14 -07001247 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001248
James E. Blaire1767bc2016-08-02 10:00:27 -07001249 self.launch_server = RecordingLaunchServer(
1250 self.config, self.connections, _run_ansible=self.run_ansible)
1251 self.launch_server.start()
1252 self.history = self.launch_server.build_history
1253 self.builds = self.launch_server.running_builds
1254
1255 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001256 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001257 self.merge_client = zuul.merger.client.MergeClient(
1258 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001259 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001260 self.zk = zuul.zk.ZooKeeper()
1261 self.zk.connect([self.zk_config])
1262
1263 self.fake_nodepool = FakeNodepool(self.zk_config.host,
1264 self.zk_config.port,
1265 self.zk_config.chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001266
James E. Blaire1767bc2016-08-02 10:00:27 -07001267 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001268 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001269 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001270 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001271
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001272 self.webapp = zuul.webapp.WebApp(
1273 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001274 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001275
1276 self.sched.start()
1277 self.sched.reconfigure(self.config)
1278 self.sched.resume()
1279 self.webapp.start()
1280 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001281 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001282
Clark Boylanb640e052014-04-03 16:41:46 -07001283 self.addCleanup(self.shutdown)
1284
James E. Blaire18d4602017-01-05 11:17:28 -08001285 def tearDown(self):
1286 super(ZuulTestCase, self).tearDown()
1287 self.assertFinalState()
1288
James E. Blairfef78942016-03-11 16:28:56 -08001289 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001290 # Register connections from the config
1291 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001292
Joshua Hesketh352264b2015-08-11 23:42:08 +10001293 def FakeSMTPFactory(*args, **kw):
1294 args = [self.smtp_messages] + list(args)
1295 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001296
Joshua Hesketh352264b2015-08-11 23:42:08 +10001297 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001298
Joshua Hesketh352264b2015-08-11 23:42:08 +10001299 # Set a changes database so multiple FakeGerrit's can report back to
1300 # a virtual canonical database given by the configured hostname
1301 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001302 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001303
Joshua Hesketh352264b2015-08-11 23:42:08 +10001304 for section_name in self.config.sections():
1305 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1306 section_name, re.I)
1307 if not con_match:
1308 continue
1309 con_name = con_match.group(2)
1310 con_config = dict(self.config.items(section_name))
1311
1312 if 'driver' not in con_config:
1313 raise Exception("No driver specified for connection %s."
1314 % con_name)
1315
1316 con_driver = con_config['driver']
1317
1318 # TODO(jhesketh): load the required class automatically
1319 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001320 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1321 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001322 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001323 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001324 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001325 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001326 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001327 self.event_queues.append(
1328 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001329 setattr(self, 'fake_' + con_name,
1330 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001331 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001332 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001333 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1334 else:
1335 raise Exception("Unknown driver, %s, for connection %s"
1336 % (con_config['driver'], con_name))
1337
1338 # If the [gerrit] or [smtp] sections still exist, load them in as a
1339 # connection named 'gerrit' or 'smtp' respectfully
1340
1341 if 'gerrit' in self.config.sections():
1342 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001343 self.event_queues.append(
1344 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001345 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001346 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001347 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001348
1349 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001350 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001351 zuul.connection.smtp.SMTPConnection(
1352 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001353
James E. Blair83005782015-12-11 14:46:03 -08001354 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001355 # This creates the per-test configuration object. It can be
1356 # overriden by subclasses, but should not need to be since it
1357 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001358 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001359 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001360 if hasattr(self, 'tenant_config_file'):
1361 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001362 git_path = os.path.join(
1363 os.path.dirname(
1364 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1365 'git')
1366 if os.path.exists(git_path):
1367 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001368 project = reponame.replace('_', '/')
1369 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001370 os.path.join(git_path, reponame))
1371
James E. Blair498059b2016-12-20 13:50:13 -08001372 def setupZK(self):
1373 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blairdce6cea2016-12-20 16:45:32 -08001374 self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
1375 self.zk_chroot_fixture.zookeeper_host,
1376 self.zk_chroot_fixture.zookeeper_port,
1377 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001378
James E. Blair96c6bf82016-01-15 16:20:40 -08001379 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001380 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001381
1382 files = {}
1383 for (dirpath, dirnames, filenames) in os.walk(source_path):
1384 for filename in filenames:
1385 test_tree_filepath = os.path.join(dirpath, filename)
1386 common_path = os.path.commonprefix([test_tree_filepath,
1387 source_path])
1388 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1389 with open(test_tree_filepath, 'r') as f:
1390 content = f.read()
1391 files[relative_filepath] = content
1392 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001393 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001394
James E. Blaire18d4602017-01-05 11:17:28 -08001395 def assertNodepoolState(self):
1396 # Make sure that there are no pending requests
1397
1398 requests = self.fake_nodepool.getNodeRequests()
1399 self.assertEqual(len(requests), 0)
1400
1401 nodes = self.fake_nodepool.getNodes()
1402 for node in nodes:
1403 self.assertFalse(node['_lock'], "Node %s is locked" %
1404 (node['_oid'],))
1405
Clark Boylanb640e052014-04-03 16:41:46 -07001406 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001407 # Make sure that git.Repo objects have been garbage collected.
1408 repos = []
1409 gc.collect()
1410 for obj in gc.get_objects():
1411 if isinstance(obj, git.Repo):
1412 repos.append(obj)
1413 self.assertEqual(len(repos), 0)
1414 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001415 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001416 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001417 for tenant in self.sched.abide.tenants.values():
1418 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001419 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001420 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001421
1422 def shutdown(self):
1423 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001424 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001425 self.merge_server.stop()
1426 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001427 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001428 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001429 self.sched.stop()
1430 self.sched.join()
1431 self.statsd.stop()
1432 self.statsd.join()
1433 self.webapp.stop()
1434 self.webapp.join()
1435 self.rpc.stop()
1436 self.rpc.join()
1437 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001438 self.fake_nodepool.stop()
1439 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001440 threads = threading.enumerate()
1441 if len(threads) > 1:
1442 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001443 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001444
1445 def init_repo(self, project):
1446 parts = project.split('/')
1447 path = os.path.join(self.upstream_root, *parts[:-1])
1448 if not os.path.exists(path):
1449 os.makedirs(path)
1450 path = os.path.join(self.upstream_root, project)
1451 repo = git.Repo.init(path)
1452
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001453 with repo.config_writer() as config_writer:
1454 config_writer.set_value('user', 'email', 'user@example.com')
1455 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001456
Clark Boylanb640e052014-04-03 16:41:46 -07001457 repo.index.commit('initial commit')
1458 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001459
James E. Blair97d902e2014-08-21 13:25:56 -07001460 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001461 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001462 repo.git.clean('-x', '-f', '-d')
1463
James E. Blair97d902e2014-08-21 13:25:56 -07001464 def create_branch(self, project, branch):
1465 path = os.path.join(self.upstream_root, project)
1466 repo = git.Repo.init(path)
1467 fn = os.path.join(path, 'README')
1468
1469 branch_head = repo.create_head(branch)
1470 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001471 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001472 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001473 f.close()
1474 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001475 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001476
James E. Blair97d902e2014-08-21 13:25:56 -07001477 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001478 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001479 repo.git.clean('-x', '-f', '-d')
1480
Sachi King9f16d522016-03-16 12:20:45 +11001481 def create_commit(self, project):
1482 path = os.path.join(self.upstream_root, project)
1483 repo = git.Repo(path)
1484 repo.head.reference = repo.heads['master']
1485 file_name = os.path.join(path, 'README')
1486 with open(file_name, 'a') as f:
1487 f.write('creating fake commit\n')
1488 repo.index.add([file_name])
1489 commit = repo.index.commit('Creating a fake commit')
1490 return commit.hexsha
1491
James E. Blairb8c16472015-05-05 14:55:26 -07001492 def orderedRelease(self):
1493 # Run one build at a time to ensure non-race order:
1494 while len(self.builds):
1495 self.release(self.builds[0])
1496 self.waitUntilSettled()
1497
Clark Boylanb640e052014-04-03 16:41:46 -07001498 def release(self, job):
1499 if isinstance(job, FakeBuild):
1500 job.release()
1501 else:
1502 job.waiting = False
1503 self.log.debug("Queued job %s released" % job.unique)
1504 self.gearman_server.wakeConnections()
1505
1506 def getParameter(self, job, name):
1507 if isinstance(job, FakeBuild):
1508 return job.parameters[name]
1509 else:
1510 parameters = json.loads(job.arguments)
1511 return parameters[name]
1512
Clark Boylanb640e052014-04-03 16:41:46 -07001513 def haveAllBuildsReported(self):
1514 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001515 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001516 return False
1517 # Find out if every build that the worker has completed has been
1518 # reported back to Zuul. If it hasn't then that means a Gearman
1519 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001520 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001521 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001522 if not zbuild:
1523 # It has already been reported
1524 continue
1525 # It hasn't been reported yet.
1526 return False
1527 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001528 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001529 if connection.state == 'GRAB_WAIT':
1530 return False
1531 return True
1532
1533 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001534 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001535 for build in builds:
1536 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001537 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001538 for j in conn.related_jobs.values():
1539 if j.unique == build.uuid:
1540 client_job = j
1541 break
1542 if not client_job:
1543 self.log.debug("%s is not known to the gearman client" %
1544 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001545 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001546 if not client_job.handle:
1547 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001548 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001549 server_job = self.gearman_server.jobs.get(client_job.handle)
1550 if not server_job:
1551 self.log.debug("%s is not known to the gearman server" %
1552 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001553 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001554 if not hasattr(server_job, 'waiting'):
1555 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001556 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001557 if server_job.waiting:
1558 continue
James E. Blair17302972016-08-10 16:11:42 -07001559 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001560 self.log.debug("%s has not reported start" % build)
1561 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001562 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001563 if worker_build:
1564 if worker_build.isWaiting():
1565 continue
1566 else:
1567 self.log.debug("%s is running" % worker_build)
1568 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001569 else:
James E. Blair962220f2016-08-03 11:22:38 -07001570 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001571 return False
1572 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001573
James E. Blairdce6cea2016-12-20 16:45:32 -08001574 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001575 if self.fake_nodepool.paused:
1576 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001577 if self.sched.nodepool.requests:
1578 return False
1579 return True
1580
Jan Hruban6b71aff2015-10-22 16:58:08 +02001581 def eventQueuesEmpty(self):
1582 for queue in self.event_queues:
1583 yield queue.empty()
1584
1585 def eventQueuesJoin(self):
1586 for queue in self.event_queues:
1587 queue.join()
1588
Clark Boylanb640e052014-04-03 16:41:46 -07001589 def waitUntilSettled(self):
1590 self.log.debug("Waiting until settled...")
1591 start = time.time()
1592 while True:
1593 if time.time() - start > 10:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001594 self.log.error("Timeout waiting for Zuul to settle")
1595 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001596 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001597 self.log.error(" %s: %s" % (queue, queue.empty()))
1598 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001599 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001600 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001601 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001602 self.log.error("All requests completed: %s" %
1603 (self.areAllNodeRequestsComplete(),))
1604 self.log.error("Merge client jobs: %s" %
1605 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001606 raise Exception("Timeout waiting for Zuul to settle")
1607 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001608
James E. Blaire1767bc2016-08-02 10:00:27 -07001609 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001610 # have all build states propogated to zuul?
1611 if self.haveAllBuildsReported():
1612 # Join ensures that the queue is empty _and_ events have been
1613 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001614 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001615 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001616 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001617 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001618 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001619 self.areAllBuildsWaiting() and
1620 self.areAllNodeRequestsComplete()):
Clark Boylanb640e052014-04-03 16:41:46 -07001621 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001622 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001623 self.log.debug("...settled.")
1624 return
1625 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001626 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001627 self.sched.wake_event.wait(0.1)
1628
1629 def countJobResults(self, jobs, result):
1630 jobs = filter(lambda x: x.result == result, jobs)
1631 return len(jobs)
1632
James E. Blair96c6bf82016-01-15 16:20:40 -08001633 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001634 for job in self.history:
1635 if (job.name == name and
1636 (project is None or
1637 job.parameters['ZUUL_PROJECT'] == project)):
1638 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001639 raise Exception("Unable to find job %s in history" % name)
1640
1641 def assertEmptyQueues(self):
1642 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001643 for tenant in self.sched.abide.tenants.values():
1644 for pipeline in tenant.layout.pipelines.values():
1645 for queue in pipeline.queues:
1646 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001647 print('pipeline %s queue %s contents %s' % (
1648 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001649 self.assertEqual(len(queue.queue), 0,
1650 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001651
1652 def assertReportedStat(self, key, value=None, kind=None):
1653 start = time.time()
1654 while time.time() < (start + 5):
1655 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001656 k, v = stat.split(':')
1657 if key == k:
1658 if value is None and kind is None:
1659 return
1660 elif value:
1661 if value == v:
1662 return
1663 elif kind:
1664 if v.endswith('|' + kind):
1665 return
1666 time.sleep(0.1)
1667
Clark Boylanb640e052014-04-03 16:41:46 -07001668 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001669
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001670 def assertBuilds(self, builds):
1671 """Assert that the running builds are as described.
1672
1673 The list of running builds is examined and must match exactly
1674 the list of builds described by the input.
1675
1676 :arg list builds: A list of dictionaries. Each item in the
1677 list must match the corresponding build in the build
1678 history, and each element of the dictionary must match the
1679 corresponding attribute of the build.
1680
1681 """
James E. Blair3158e282016-08-19 09:34:11 -07001682 try:
1683 self.assertEqual(len(self.builds), len(builds))
1684 for i, d in enumerate(builds):
1685 for k, v in d.items():
1686 self.assertEqual(
1687 getattr(self.builds[i], k), v,
1688 "Element %i in builds does not match" % (i,))
1689 except Exception:
1690 for build in self.builds:
1691 self.log.error("Running build: %s" % build)
1692 else:
1693 self.log.error("No running builds")
1694 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001695
James E. Blairb536ecc2016-08-31 10:11:42 -07001696 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001697 """Assert that the completed builds are as described.
1698
1699 The list of completed builds is examined and must match
1700 exactly the list of builds described by the input.
1701
1702 :arg list history: A list of dictionaries. Each item in the
1703 list must match the corresponding build in the build
1704 history, and each element of the dictionary must match the
1705 corresponding attribute of the build.
1706
James E. Blairb536ecc2016-08-31 10:11:42 -07001707 :arg bool ordered: If true, the history must match the order
1708 supplied, if false, the builds are permitted to have
1709 arrived in any order.
1710
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001711 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001712 def matches(history_item, item):
1713 for k, v in item.items():
1714 if getattr(history_item, k) != v:
1715 return False
1716 return True
James E. Blair3158e282016-08-19 09:34:11 -07001717 try:
1718 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001719 if ordered:
1720 for i, d in enumerate(history):
1721 if not matches(self.history[i], d):
1722 raise Exception(
1723 "Element %i in history does not match" % (i,))
1724 else:
1725 unseen = self.history[:]
1726 for i, d in enumerate(history):
1727 found = False
1728 for unseen_item in unseen:
1729 if matches(unseen_item, d):
1730 found = True
1731 unseen.remove(unseen_item)
1732 break
1733 if not found:
1734 raise Exception("No match found for element %i "
1735 "in history" % (i,))
1736 if unseen:
1737 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001738 except Exception:
1739 for build in self.history:
1740 self.log.error("Completed build: %s" % build)
1741 else:
1742 self.log.error("No completed builds")
1743 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001744
James E. Blair6ac368c2016-12-22 18:07:20 -08001745 def printHistory(self):
1746 """Log the build history.
1747
1748 This can be useful during tests to summarize what jobs have
1749 completed.
1750
1751 """
1752 self.log.debug("Build history:")
1753 for build in self.history:
1754 self.log.debug(build)
1755
James E. Blair59fdbac2015-12-07 17:08:06 -08001756 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001757 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1758
1759 def updateConfigLayout(self, path):
1760 root = os.path.join(self.test_root, "config")
1761 os.makedirs(root)
1762 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1763 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001764- tenant:
1765 name: openstack
1766 source:
1767 gerrit:
1768 config-repos:
1769 - %s
1770 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001771 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001772 self.config.set('zuul', 'tenant_config',
1773 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001774
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001775 def addCommitToRepo(self, project, message, files,
1776 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001777 path = os.path.join(self.upstream_root, project)
1778 repo = git.Repo(path)
1779 repo.head.reference = branch
1780 zuul.merger.merger.reset_repo_to_head(repo)
1781 for fn, content in files.items():
1782 fn = os.path.join(path, fn)
1783 with open(fn, 'w') as f:
1784 f.write(content)
1785 repo.index.add([fn])
1786 commit = repo.index.commit(message)
1787 repo.heads[branch].commit = commit
1788 repo.head.reference = branch
1789 repo.git.clean('-x', '-f', '-d')
1790 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001791 if tag:
1792 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001793
James E. Blair7fc8daa2016-08-08 15:37:15 -07001794 def addEvent(self, connection, event):
1795 """Inject a Fake (Gerrit) event.
1796
1797 This method accepts a JSON-encoded event and simulates Zuul
1798 having received it from Gerrit. It could (and should)
1799 eventually apply to any connection type, but is currently only
1800 used with Gerrit connections. The name of the connection is
1801 used to look up the corresponding server, and the event is
1802 simulated as having been received by all Zuul connections
1803 attached to that server. So if two Gerrit connections in Zuul
1804 are connected to the same Gerrit server, and you invoke this
1805 method specifying the name of one of them, the event will be
1806 received by both.
1807
1808 .. note::
1809
1810 "self.fake_gerrit.addEvent" calls should be migrated to
1811 this method.
1812
1813 :arg str connection: The name of the connection corresponding
1814 to the gerrit server.
1815 :arg str event: The JSON-encoded event.
1816
1817 """
1818 specified_conn = self.connections.connections[connection]
1819 for conn in self.connections.connections.values():
1820 if (isinstance(conn, specified_conn.__class__) and
1821 specified_conn.server == conn.server):
1822 conn.addEvent(event)
1823
James E. Blair3f876d52016-07-22 13:07:14 -07001824
1825class AnsibleZuulTestCase(ZuulTestCase):
1826 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001827 run_ansible = True