blob: 9ec8e5429166b7fda591c5f80c533a5d3a7d71c2 [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
24import pprint
Christian Berendt12d4d722014-06-07 21:03:45 +020025from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070026from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070027import random
28import re
29import select
30import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030031from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070032import socket
33import string
34import subprocess
35import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
38import time
Clark Boylanb640e052014-04-03 16:41:46 -070039
40import git
41import gear
42import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080043import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080044import kazoo.exceptions
Clark Boylanb640e052014-04-03 16:41:46 -070045import statsd
46import testtools
Clint Byrum3343e3e2016-11-15 16:05:03 -080047from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070048
Joshua Hesketh352264b2015-08-11 23:42:08 +100049import zuul.connection.gerrit
50import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070051import zuul.scheduler
52import zuul.webapp
53import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100054import zuul.launcher.server
55import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070056import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080057import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070058import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070059import zuul.merger.merger
60import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070061import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.reporter.gerrit
63import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110064import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070065import zuul.trigger.gerrit
66import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070067import zuul.trigger.zuultrigger
James E. Blairdce6cea2016-12-20 16:45:32 -080068import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070069
70FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
71 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070072USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070073
74logging.basicConfig(level=logging.DEBUG,
75 format='%(asctime)s %(name)-32s '
76 '%(levelname)-8s %(message)s')
77
78
79def repack_repo(path):
80 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
81 output = subprocess.Popen(cmd, close_fds=True,
82 stdout=subprocess.PIPE,
83 stderr=subprocess.PIPE)
84 out = output.communicate()
85 if output.returncode:
86 raise Exception("git repack returned %d" % output.returncode)
87 return out
88
89
90def random_sha1():
91 return hashlib.sha1(str(random.random())).hexdigest()
92
93
James E. Blaira190f3b2015-01-05 14:56:54 -080094def iterate_timeout(max_seconds, purpose):
95 start = time.time()
96 count = 0
97 while (time.time() < start + max_seconds):
98 count += 1
99 yield count
100 time.sleep(0)
101 raise Exception("Timeout waiting for %s" % purpose)
102
103
Clark Boylanb640e052014-04-03 16:41:46 -0700104class ChangeReference(git.Reference):
105 _common_path_default = "refs/changes"
106 _points_to_commits_only = True
107
108
109class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700110 categories = {'approved': ('Approved', -1, 1),
111 'code-review': ('Code-Review', -2, 2),
112 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700113
114 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700115 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700116 self.gerrit = gerrit
117 self.reported = 0
118 self.queried = 0
119 self.patchsets = []
120 self.number = number
121 self.project = project
122 self.branch = branch
123 self.subject = subject
124 self.latest_patchset = 0
125 self.depends_on_change = None
126 self.needed_by_changes = []
127 self.fail_merge = False
128 self.messages = []
129 self.data = {
130 'branch': branch,
131 'comments': [],
132 'commitMessage': subject,
133 'createdOn': time.time(),
134 'id': 'I' + random_sha1(),
135 'lastUpdated': time.time(),
136 'number': str(number),
137 'open': status == 'NEW',
138 'owner': {'email': 'user@example.com',
139 'name': 'User Name',
140 'username': 'username'},
141 'patchSets': self.patchsets,
142 'project': project,
143 'status': status,
144 'subject': subject,
145 'submitRecords': [],
146 'url': 'https://hostname/%s' % number}
147
148 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700150 self.data['submitRecords'] = self.getSubmitRecords()
151 self.open = status == 'NEW'
152
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700153 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700154 path = os.path.join(self.upstream_root, self.project)
155 repo = git.Repo(path)
156 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
157 self.latest_patchset),
158 'refs/tags/init')
159 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700160 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700161 repo.git.clean('-x', '-f', '-d')
162
163 path = os.path.join(self.upstream_root, self.project)
164 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700165 for fn, content in files.items():
166 fn = os.path.join(path, fn)
167 with open(fn, 'w') as f:
168 f.write(content)
169 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700170 else:
171 for fni in range(100):
172 fn = os.path.join(path, str(fni))
173 f = open(fn, 'w')
174 for ci in range(4096):
175 f.write(random.choice(string.printable))
176 f.close()
177 repo.index.add([fn])
178
179 r = repo.index.commit(msg)
180 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700181 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700182 repo.git.clean('-x', '-f', '-d')
183 repo.heads['master'].checkout()
184 return r
185
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700187 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700188 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700189 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 data = ("test %s %s %s\n" %
191 (self.branch, self.number, self.latest_patchset))
192 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700193 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700195 ps_files = [{'file': '/COMMIT_MSG',
196 'type': 'ADDED'},
197 {'file': 'README',
198 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700199 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700200 ps_files.append({'file': f, 'type': 'ADDED'})
201 d = {'approvals': [],
202 'createdOn': time.time(),
203 'files': ps_files,
204 'number': str(self.latest_patchset),
205 'ref': 'refs/changes/1/%s/%s' % (self.number,
206 self.latest_patchset),
207 'revision': c.hexsha,
208 'uploader': {'email': 'user@example.com',
209 'name': 'User name',
210 'username': 'user'}}
211 self.data['currentPatchSet'] = d
212 self.patchsets.append(d)
213 self.data['submitRecords'] = self.getSubmitRecords()
214
215 def getPatchsetCreatedEvent(self, patchset):
216 event = {"type": "patchset-created",
217 "change": {"project": self.project,
218 "branch": self.branch,
219 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
220 "number": str(self.number),
221 "subject": self.subject,
222 "owner": {"name": "User Name"},
223 "url": "https://hostname/3"},
224 "patchSet": self.patchsets[patchset - 1],
225 "uploader": {"name": "User Name"}}
226 return event
227
228 def getChangeRestoredEvent(self):
229 event = {"type": "change-restored",
230 "change": {"project": self.project,
231 "branch": self.branch,
232 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
233 "number": str(self.number),
234 "subject": self.subject,
235 "owner": {"name": "User Name"},
236 "url": "https://hostname/3"},
237 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100238 "patchSet": self.patchsets[-1],
239 "reason": ""}
240 return event
241
242 def getChangeAbandonedEvent(self):
243 event = {"type": "change-abandoned",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "abandoner": {"name": "User Name"},
252 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700253 "reason": ""}
254 return event
255
256 def getChangeCommentEvent(self, patchset):
257 event = {"type": "comment-added",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "patchSet": self.patchsets[patchset - 1],
266 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700267 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700268 "description": "Code-Review",
269 "value": "0"}],
270 "comment": "This is a comment"}
271 return event
272
Joshua Hesketh642824b2014-07-01 17:54:59 +1000273 def addApproval(self, category, value, username='reviewer_john',
274 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700275 if not granted_on:
276 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000277 approval = {
278 'description': self.categories[category][0],
279 'type': category,
280 'value': str(value),
281 'by': {
282 'username': username,
283 'email': username + '@example.com',
284 },
285 'grantedOn': int(granted_on)
286 }
Clark Boylanb640e052014-04-03 16:41:46 -0700287 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
288 if x['by']['username'] == username and x['type'] == category:
289 del self.patchsets[-1]['approvals'][i]
290 self.patchsets[-1]['approvals'].append(approval)
291 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000292 'author': {'email': 'author@example.com',
293 'name': 'Patchset Author',
294 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700295 'change': {'branch': self.branch,
296 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
297 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000298 'owner': {'email': 'owner@example.com',
299 'name': 'Change Owner',
300 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700301 'project': self.project,
302 'subject': self.subject,
303 'topic': 'master',
304 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000305 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700306 'patchSet': self.patchsets[-1],
307 'type': 'comment-added'}
308 self.data['submitRecords'] = self.getSubmitRecords()
309 return json.loads(json.dumps(event))
310
311 def getSubmitRecords(self):
312 status = {}
313 for cat in self.categories.keys():
314 status[cat] = 0
315
316 for a in self.patchsets[-1]['approvals']:
317 cur = status[a['type']]
318 cat_min, cat_max = self.categories[a['type']][1:]
319 new = int(a['value'])
320 if new == cat_min:
321 cur = new
322 elif abs(new) > abs(cur):
323 cur = new
324 status[a['type']] = cur
325
326 labels = []
327 ok = True
328 for typ, cat in self.categories.items():
329 cur = status[typ]
330 cat_min, cat_max = cat[1:]
331 if cur == cat_min:
332 value = 'REJECT'
333 ok = False
334 elif cur == cat_max:
335 value = 'OK'
336 else:
337 value = 'NEED'
338 ok = False
339 labels.append({'label': cat[0], 'status': value})
340 if ok:
341 return [{'status': 'OK'}]
342 return [{'status': 'NOT_READY',
343 'labels': labels}]
344
345 def setDependsOn(self, other, patchset):
346 self.depends_on_change = other
347 d = {'id': other.data['id'],
348 'number': other.data['number'],
349 'ref': other.patchsets[patchset - 1]['ref']
350 }
351 self.data['dependsOn'] = [d]
352
353 other.needed_by_changes.append(self)
354 needed = other.data.get('neededBy', [])
355 d = {'id': self.data['id'],
356 'number': self.data['number'],
357 'ref': self.patchsets[patchset - 1]['ref'],
358 'revision': self.patchsets[patchset - 1]['revision']
359 }
360 needed.append(d)
361 other.data['neededBy'] = needed
362
363 def query(self):
364 self.queried += 1
365 d = self.data.get('dependsOn')
366 if d:
367 d = d[0]
368 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
369 d['isCurrentPatchSet'] = True
370 else:
371 d['isCurrentPatchSet'] = False
372 return json.loads(json.dumps(self.data))
373
374 def setMerged(self):
375 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000376 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700377 return
378 if self.fail_merge:
379 return
380 self.data['status'] = 'MERGED'
381 self.open = False
382
383 path = os.path.join(self.upstream_root, self.project)
384 repo = git.Repo(path)
385 repo.heads[self.branch].commit = \
386 repo.commit(self.patchsets[-1]['revision'])
387
388 def setReported(self):
389 self.reported += 1
390
391
Joshua Hesketh352264b2015-08-11 23:42:08 +1000392class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700393 """A Fake Gerrit connection for use in tests.
394
395 This subclasses
396 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
397 ability for tests to add changes to the fake Gerrit it represents.
398 """
399
Joshua Hesketh352264b2015-08-11 23:42:08 +1000400 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700401
Joshua Hesketh352264b2015-08-11 23:42:08 +1000402 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700403 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000404 super(FakeGerritConnection, self).__init__(connection_name,
405 connection_config)
406
James E. Blair7fc8daa2016-08-08 15:37:15 -0700407 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700408 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
409 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000410 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700411 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200412 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700413
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700414 def addFakeChange(self, project, branch, subject, status='NEW',
415 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700416 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700417 self.change_number += 1
418 c = FakeChange(self, self.change_number, project, branch, subject,
419 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700420 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700421 self.changes[self.change_number] = c
422 return c
423
Clark Boylanb640e052014-04-03 16:41:46 -0700424 def review(self, project, changeid, message, action):
425 number, ps = changeid.split(',')
426 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000427
428 # Add the approval back onto the change (ie simulate what gerrit would
429 # do).
430 # Usually when zuul leaves a review it'll create a feedback loop where
431 # zuul's review enters another gerrit event (which is then picked up by
432 # zuul). However, we can't mimic this behaviour (by adding this
433 # approval event into the queue) as it stops jobs from checking what
434 # happens before this event is triggered. If a job needs to see what
435 # happens they can add their own verified event into the queue.
436 # Nevertheless, we can update change with the new review in gerrit.
437
James E. Blair8b5408c2016-08-08 15:37:46 -0700438 for cat in action.keys():
439 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000440 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000441
James E. Blair8b5408c2016-08-08 15:37:46 -0700442 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000443 if 'label' in action:
444 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000445 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000446
Clark Boylanb640e052014-04-03 16:41:46 -0700447 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000448
Clark Boylanb640e052014-04-03 16:41:46 -0700449 if 'submit' in action:
450 change.setMerged()
451 if message:
452 change.setReported()
453
454 def query(self, number):
455 change = self.changes.get(int(number))
456 if change:
457 return change.query()
458 return {}
459
James E. Blairc494d542014-08-06 09:23:52 -0700460 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700461 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700462 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800463 if query.startswith('change:'):
464 # Query a specific changeid
465 changeid = query[len('change:'):]
466 l = [change.query() for change in self.changes.values()
467 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700468 elif query.startswith('message:'):
469 # Query the content of a commit message
470 msg = query[len('message:'):].strip()
471 l = [change.query() for change in self.changes.values()
472 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800473 else:
474 # Query all open changes
475 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700476 return l
James E. Blairc494d542014-08-06 09:23:52 -0700477
Joshua Hesketh352264b2015-08-11 23:42:08 +1000478 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700479 pass
480
Joshua Hesketh352264b2015-08-11 23:42:08 +1000481 def getGitUrl(self, project):
482 return os.path.join(self.upstream_root, project.name)
483
Adam Gandelmanc5e4f1d2016-11-29 14:27:17 -0800484 def _getGitwebUrl(self, project, sha=None):
485 return self.getGitwebUrl(project, sha)
486
Clark Boylanb640e052014-04-03 16:41:46 -0700487
488class BuildHistory(object):
489 def __init__(self, **kw):
490 self.__dict__.update(kw)
491
492 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700493 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
494 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700495
496
497class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200498 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700499 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700500 self.url = url
501
502 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700503 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700504 path = res.path
505 project = '/'.join(path.split('/')[2:-2])
506 ret = '001e# service=git-upload-pack\n'
507 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
508 'multi_ack thin-pack side-band side-band-64k ofs-delta '
509 'shallow no-progress include-tag multi_ack_detailed no-done\n')
510 path = os.path.join(self.upstream_root, project)
511 repo = git.Repo(path)
512 for ref in repo.refs:
513 r = ref.object.hexsha + ' ' + ref.path + '\n'
514 ret += '%04x%s' % (len(r) + 4, r)
515 ret += '0000'
516 return ret
517
518
Clark Boylanb640e052014-04-03 16:41:46 -0700519class FakeStatsd(threading.Thread):
520 def __init__(self):
521 threading.Thread.__init__(self)
522 self.daemon = True
523 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
524 self.sock.bind(('', 0))
525 self.port = self.sock.getsockname()[1]
526 self.wake_read, self.wake_write = os.pipe()
527 self.stats = []
528
529 def run(self):
530 while True:
531 poll = select.poll()
532 poll.register(self.sock, select.POLLIN)
533 poll.register(self.wake_read, select.POLLIN)
534 ret = poll.poll()
535 for (fd, event) in ret:
536 if fd == self.sock.fileno():
537 data = self.sock.recvfrom(1024)
538 if not data:
539 return
540 self.stats.append(data[0])
541 if fd == self.wake_read:
542 return
543
544 def stop(self):
545 os.write(self.wake_write, '1\n')
546
547
James E. Blaire1767bc2016-08-02 10:00:27 -0700548class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700549 log = logging.getLogger("zuul.test")
550
James E. Blair34776ee2016-08-25 13:53:54 -0700551 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700552 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700553 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700554 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700555 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700556 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700557 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700558 # TODOv3(jeblair): self.node is really "the image of the node
559 # assigned". We should rename it (self.node_image?) if we
560 # keep using it like this, or we may end up exposing more of
561 # the complexity around multi-node jobs here
562 # (self.nodes[0].image?)
563 self.node = None
564 if len(self.parameters.get('nodes')) == 1:
565 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700566 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700567 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700568 self.wait_condition = threading.Condition()
569 self.waiting = False
570 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500571 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700572 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700573 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700574 self.changes = None
575 if 'ZUUL_CHANGE_IDS' in self.parameters:
576 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700577
James E. Blair3158e282016-08-19 09:34:11 -0700578 def __repr__(self):
579 waiting = ''
580 if self.waiting:
581 waiting = ' [waiting]'
582 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
583
Clark Boylanb640e052014-04-03 16:41:46 -0700584 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700585 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700586 self.wait_condition.acquire()
587 self.wait_condition.notify()
588 self.waiting = False
589 self.log.debug("Build %s released" % self.unique)
590 self.wait_condition.release()
591
592 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700593 """Return whether this build is being held.
594
595 :returns: Whether the build is being held.
596 :rtype: bool
597 """
598
Clark Boylanb640e052014-04-03 16:41:46 -0700599 self.wait_condition.acquire()
600 if self.waiting:
601 ret = True
602 else:
603 ret = False
604 self.wait_condition.release()
605 return ret
606
607 def _wait(self):
608 self.wait_condition.acquire()
609 self.waiting = True
610 self.log.debug("Build %s waiting" % self.unique)
611 self.wait_condition.wait()
612 self.wait_condition.release()
613
614 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700615 self.log.debug('Running build %s' % self.unique)
616
James E. Blaire1767bc2016-08-02 10:00:27 -0700617 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700618 self.log.debug('Holding build %s' % self.unique)
619 self._wait()
620 self.log.debug("Build %s continuing" % self.unique)
621
Clark Boylanb640e052014-04-03 16:41:46 -0700622 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700623 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700624 result = 'FAILURE'
625 if self.aborted:
626 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500627 if self.requeue:
628 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700629
630 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700631 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700632
James E. Blaire1767bc2016-08-02 10:00:27 -0700633 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700634
James E. Blaira5dba232016-08-08 15:53:24 -0700635 def shouldFail(self):
636 changes = self.launch_server.fail_tests.get(self.name, [])
637 for change in changes:
638 if self.hasChanges(change):
639 return True
640 return False
641
James E. Blaire7b99a02016-08-05 14:27:34 -0700642 def hasChanges(self, *changes):
643 """Return whether this build has certain changes in its git repos.
644
645 :arg FakeChange changes: One or more changes (varargs) that
646 are expected to be present (in order) in the git repository of
647 the active project.
648
649 :returns: Whether the build has the indicated changes.
650 :rtype: bool
651
652 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800653 for change in changes:
654 path = os.path.join(self.jobdir.git_root, change.project)
655 try:
656 repo = git.Repo(path)
657 except NoSuchPathError as e:
658 self.log.debug('%s' % e)
659 return False
660 ref = self.parameters['ZUUL_REF']
661 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
662 commit_message = '%s-1' % change.subject
663 self.log.debug("Checking if build %s has changes; commit_message "
664 "%s; repo_messages %s" % (self, commit_message,
665 repo_messages))
666 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700667 self.log.debug(" messages do not match")
668 return False
669 self.log.debug(" OK")
670 return True
671
Clark Boylanb640e052014-04-03 16:41:46 -0700672
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000673class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700674 """An Ansible launcher to be used in tests.
675
676 :ivar bool hold_jobs_in_build: If true, when jobs are launched
677 they will report that they have started but then pause until
678 released before reporting completion. This attribute may be
679 changed at any time and will take effect for subsequently
680 launched builds, but previously held builds will still need to
681 be explicitly released.
682
683 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800684 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700685 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800686 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700687 self.hold_jobs_in_build = False
688 self.lock = threading.Lock()
689 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700690 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700691 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700692 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800693
James E. Blaira5dba232016-08-08 15:53:24 -0700694 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700695 """Instruct the launcher to report matching builds as failures.
696
697 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700698 :arg Change change: The :py:class:`~tests.base.FakeChange`
699 instance which should cause the job to fail. This job
700 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700701
702 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700703 l = self.fail_tests.get(name, [])
704 l.append(change)
705 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800706
James E. Blair962220f2016-08-03 11:22:38 -0700707 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700708 """Release a held build.
709
710 :arg str regex: A regular expression which, if supplied, will
711 cause only builds with matching names to be released. If
712 not supplied, all builds will be released.
713
714 """
James E. Blair962220f2016-08-03 11:22:38 -0700715 builds = self.running_builds[:]
716 self.log.debug("Releasing build %s (%s)" % (regex,
717 len(self.running_builds)))
718 for build in builds:
719 if not regex or re.match(regex, build.name):
720 self.log.debug("Releasing build %s" %
721 (build.parameters['ZUUL_UUID']))
722 build.release()
723 else:
724 self.log.debug("Not releasing build %s" %
725 (build.parameters['ZUUL_UUID']))
726 self.log.debug("Done releasing builds %s (%s)" %
727 (regex, len(self.running_builds)))
728
James E. Blair17302972016-08-10 16:11:42 -0700729 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700730 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700731 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700732 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700733 self.job_builds[job.unique] = build
James E. Blair17302972016-08-10 16:11:42 -0700734 super(RecordingLaunchServer, self).launchJob(job)
735
736 def stopJob(self, job):
737 self.log.debug("handle stop")
738 parameters = json.loads(job.arguments)
739 uuid = parameters['uuid']
740 for build in self.running_builds:
741 if build.unique == uuid:
742 build.aborted = True
743 build.release()
744 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700745
746 def runAnsible(self, jobdir, job):
747 build = self.job_builds[job.unique]
748 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700749
750 if self._run_ansible:
751 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
752 else:
753 result = build.run()
754
755 self.lock.acquire()
756 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700757 BuildHistory(name=build.name, result=result, changes=build.changes,
758 node=build.node, uuid=build.unique,
759 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700760 pipeline=build.parameters['ZUUL_PIPELINE'])
761 )
James E. Blairab7132b2016-08-05 12:36:22 -0700762 self.running_builds.remove(build)
763 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700764 self.lock.release()
Clint Byrum69e47122016-12-02 16:40:35 -0800765 if build.run_error:
766 result = None
James E. Blaire1767bc2016-08-02 10:00:27 -0700767 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800768
769
Clark Boylanb640e052014-04-03 16:41:46 -0700770class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700771 """A Gearman server for use in tests.
772
773 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
774 added to the queue but will not be distributed to workers
775 until released. This attribute may be changed at any time and
776 will take effect for subsequently enqueued jobs, but
777 previously held jobs will still need to be explicitly
778 released.
779
780 """
781
Clark Boylanb640e052014-04-03 16:41:46 -0700782 def __init__(self):
783 self.hold_jobs_in_queue = False
784 super(FakeGearmanServer, self).__init__(0)
785
786 def getJobForConnection(self, connection, peek=False):
787 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
788 for job in queue:
789 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500790 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700791 job.waiting = self.hold_jobs_in_queue
792 else:
793 job.waiting = False
794 if job.waiting:
795 continue
796 if job.name in connection.functions:
797 if not peek:
798 queue.remove(job)
799 connection.related_jobs[job.handle] = job
800 job.worker_connection = connection
801 job.running = True
802 return job
803 return None
804
805 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700806 """Release a held job.
807
808 :arg str regex: A regular expression which, if supplied, will
809 cause only jobs with matching names to be released. If
810 not supplied, all jobs will be released.
811 """
Clark Boylanb640e052014-04-03 16:41:46 -0700812 released = False
813 qlen = (len(self.high_queue) + len(self.normal_queue) +
814 len(self.low_queue))
815 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
816 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500817 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700818 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500819 parameters = json.loads(job.arguments)
820 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700821 self.log.debug("releasing queued job %s" %
822 job.unique)
823 job.waiting = False
824 released = True
825 else:
826 self.log.debug("not releasing queued job %s" %
827 job.unique)
828 if released:
829 self.wakeConnections()
830 qlen = (len(self.high_queue) + len(self.normal_queue) +
831 len(self.low_queue))
832 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
833
834
835class FakeSMTP(object):
836 log = logging.getLogger('zuul.FakeSMTP')
837
838 def __init__(self, messages, server, port):
839 self.server = server
840 self.port = port
841 self.messages = messages
842
843 def sendmail(self, from_email, to_email, msg):
844 self.log.info("Sending email from %s, to %s, with msg %s" % (
845 from_email, to_email, msg))
846
847 headers = msg.split('\n\n', 1)[0]
848 body = msg.split('\n\n', 1)[1]
849
850 self.messages.append(dict(
851 from_email=from_email,
852 to_email=to_email,
853 msg=msg,
854 headers=headers,
855 body=body,
856 ))
857
858 return True
859
860 def quit(self):
861 return True
862
863
864class FakeSwiftClientConnection(swiftclient.client.Connection):
865 def post_account(self, headers):
866 # Do nothing
867 pass
868
869 def get_auth(self):
870 # Returns endpoint and (unused) auth token
871 endpoint = os.path.join('https://storage.example.org', 'V1',
872 'AUTH_account')
873 return endpoint, ''
874
875
James E. Blairdce6cea2016-12-20 16:45:32 -0800876class FakeNodepool(object):
877 REQUEST_ROOT = '/nodepool/requests'
878
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. Blaira38c28e2017-01-04 10:33:20 -0800922 def makeNode(self, request_id, node_type):
923 now = time.time()
924 path = '/nodepool/nodes/'
925 data = dict(type=node_type,
926 provider='test-provider',
927 region='test-region',
928 az=None,
929 public_ipv4='127.0.0.1',
930 private_ipv4=None,
931 public_ipv6=None,
932 allocated_to=request_id,
933 state='ready',
934 state_time=now,
935 created_time=now,
936 updated_time=now,
937 image_id=None,
938 launcher='fake-nodepool')
939 data = json.dumps(data)
940 path = self.client.create(path, data,
941 makepath=True,
942 sequence=True)
943 nodeid = path.split("/")[-1]
944 return nodeid
945
James E. Blairdce6cea2016-12-20 16:45:32 -0800946 def fulfillRequest(self, request):
947 if request['state'] == 'fulfilled':
948 return
949 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -0800950 oid = request['_oid']
951 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -0800952
953 nodes = []
954 for node in request['node_types']:
955 nodeid = self.makeNode(oid, node)
956 nodes.append(nodeid)
957
958 request['state'] = 'fulfilled'
959 request['state_time'] = time.time()
960 request['nodes'] = nodes
James E. Blairdce6cea2016-12-20 16:45:32 -0800961 path = self.REQUEST_ROOT + '/' + oid
962 data = json.dumps(request)
963 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
964 self.client.set(path, data)
965
966
James E. Blair498059b2016-12-20 13:50:13 -0800967class ChrootedKazooFixture(fixtures.Fixture):
968 def __init__(self):
969 super(ChrootedKazooFixture, self).__init__()
970
971 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
972 if ':' in zk_host:
973 host, port = zk_host.split(':')
974 else:
975 host = zk_host
976 port = None
977
978 self.zookeeper_host = host
979
980 if not port:
981 self.zookeeper_port = 2181
982 else:
983 self.zookeeper_port = int(port)
984
985 def _setUp(self):
986 # Make sure the test chroot paths do not conflict
987 random_bits = ''.join(random.choice(string.ascii_lowercase +
988 string.ascii_uppercase)
989 for x in range(8))
990
991 rand_test_path = '%s_%s' % (random_bits, os.getpid())
992 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
993
994 # Ensure the chroot path exists and clean up any pre-existing znodes.
995 _tmp_client = kazoo.client.KazooClient(
996 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
997 _tmp_client.start()
998
999 if _tmp_client.exists(self.zookeeper_chroot):
1000 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1001
1002 _tmp_client.ensure_path(self.zookeeper_chroot)
1003 _tmp_client.stop()
1004 _tmp_client.close()
1005
1006 self.addCleanup(self._cleanup)
1007
1008 def _cleanup(self):
1009 '''Remove the chroot path.'''
1010 # Need a non-chroot'ed client to remove the chroot path
1011 _tmp_client = kazoo.client.KazooClient(
1012 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1013 _tmp_client.start()
1014 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1015 _tmp_client.stop()
1016
1017
Maru Newby3fe5f852015-01-13 04:22:14 +00001018class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001019 log = logging.getLogger("zuul.test")
1020
1021 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001022 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001023 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1024 try:
1025 test_timeout = int(test_timeout)
1026 except ValueError:
1027 # If timeout value is invalid do not set a timeout.
1028 test_timeout = 0
1029 if test_timeout > 0:
1030 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1031
1032 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1033 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1034 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1035 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1036 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1037 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1038 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1039 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1040 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1041 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -07001042 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001043 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
1044 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -07001045 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
1046 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001047 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
1048 log_level = logging.WARNING
1049 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
1050 log_level = logging.ERROR
1051 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
1052 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -07001053 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +01001054 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -07001055 format='%(asctime)s %(name)-32s '
1056 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +00001057
James E. Blairdce6cea2016-12-20 16:45:32 -08001058 # NOTE(notmorgan): Extract logging overrides for specific libraries
1059 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
1060 # each. This is used to limit the output during test runs from
1061 # libraries that zuul depends on such as gear.
1062 log_defaults_from_env = os.environ.get(
1063 'OS_LOG_DEFAULTS',
1064 'git.cmd=INFO,kazoo.client=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001065
James E. Blairdce6cea2016-12-20 16:45:32 -08001066 if log_defaults_from_env:
1067 for default in log_defaults_from_env.split(','):
1068 try:
1069 name, level_str = default.split('=', 1)
1070 level = getattr(logging, level_str, logging.DEBUG)
1071 self.useFixture(fixtures.FakeLogger(
1072 name=name,
1073 level=level,
1074 format='%(asctime)s %(name)-32s '
1075 '%(levelname)-8s %(message)s'))
1076 except ValueError:
1077 # NOTE(notmorgan): Invalid format of the log default,
1078 # skip and don't try and apply a logger for the
1079 # specified module
1080 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001081
Maru Newby3fe5f852015-01-13 04:22:14 +00001082
1083class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001084 """A test case with a functioning Zuul.
1085
1086 The following class variables are used during test setup and can
1087 be overidden by subclasses but are effectively read-only once a
1088 test method starts running:
1089
1090 :cvar str config_file: This points to the main zuul config file
1091 within the fixtures directory. Subclasses may override this
1092 to obtain a different behavior.
1093
1094 :cvar str tenant_config_file: This is the tenant config file
1095 (which specifies from what git repos the configuration should
1096 be loaded). It defaults to the value specified in
1097 `config_file` but can be overidden by subclasses to obtain a
1098 different tenant/project layout while using the standard main
1099 configuration.
1100
1101 The following are instance variables that are useful within test
1102 methods:
1103
1104 :ivar FakeGerritConnection fake_<connection>:
1105 A :py:class:`~tests.base.FakeGerritConnection` will be
1106 instantiated for each connection present in the config file
1107 and stored here. For instance, `fake_gerrit` will hold the
1108 FakeGerritConnection object for a connection named `gerrit`.
1109
1110 :ivar FakeGearmanServer gearman_server: An instance of
1111 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1112 server that all of the Zuul components in this test use to
1113 communicate with each other.
1114
1115 :ivar RecordingLaunchServer launch_server: An instance of
1116 :py:class:`~tests.base.RecordingLaunchServer` which is the
1117 Ansible launch server used to run jobs for this test.
1118
1119 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1120 representing currently running builds. They are appended to
1121 the list in the order they are launched, and removed from this
1122 list upon completion.
1123
1124 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1125 objects representing completed builds. They are appended to
1126 the list in the order they complete.
1127
1128 """
1129
James E. Blair83005782015-12-11 14:46:03 -08001130 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001131 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001132
1133 def _startMerger(self):
1134 self.merge_server = zuul.merger.server.MergeServer(self.config,
1135 self.connections)
1136 self.merge_server.start()
1137
Maru Newby3fe5f852015-01-13 04:22:14 +00001138 def setUp(self):
1139 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001140
1141 self.setupZK()
1142
James E. Blair97d902e2014-08-21 13:25:56 -07001143 if USE_TEMPDIR:
1144 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001145 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1146 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001147 else:
1148 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001149 self.test_root = os.path.join(tmp_root, "zuul-test")
1150 self.upstream_root = os.path.join(self.test_root, "upstream")
1151 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001152 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001153
1154 if os.path.exists(self.test_root):
1155 shutil.rmtree(self.test_root)
1156 os.makedirs(self.test_root)
1157 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001158 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001159
1160 # Make per test copy of Configuration.
1161 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001162 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001163 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001164 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001165 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001166 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001167
1168 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001169 # TODOv3(jeblair): remove these and replace with new git
1170 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001171 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001172 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001173 self.init_repo("org/project5")
1174 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001175 self.init_repo("org/one-job-project")
1176 self.init_repo("org/nonvoting-project")
1177 self.init_repo("org/templated-project")
1178 self.init_repo("org/layered-project")
1179 self.init_repo("org/node-project")
1180 self.init_repo("org/conflict-project")
1181 self.init_repo("org/noop-project")
1182 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001183 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001184
1185 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001186 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1187 # see: https://github.com/jsocol/pystatsd/issues/61
1188 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001189 os.environ['STATSD_PORT'] = str(self.statsd.port)
1190 self.statsd.start()
1191 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001192 reload_module(statsd)
1193 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001194
1195 self.gearman_server = FakeGearmanServer()
1196
1197 self.config.set('gearman', 'port', str(self.gearman_server.port))
1198
Joshua Hesketh352264b2015-08-11 23:42:08 +10001199 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1200 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1201 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001202
Joshua Hesketh352264b2015-08-11 23:42:08 +10001203 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001204
1205 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1206 FakeSwiftClientConnection))
1207 self.swift = zuul.lib.swift.Swift(self.config)
1208
Jan Hruban6b71aff2015-10-22 16:58:08 +02001209 self.event_queues = [
1210 self.sched.result_event_queue,
1211 self.sched.trigger_event_queue
1212 ]
1213
James E. Blairfef78942016-03-11 16:28:56 -08001214 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001215 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001216
Clark Boylanb640e052014-04-03 16:41:46 -07001217 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001218 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001219 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001220 return FakeURLOpener(self.upstream_root, *args, **kw)
1221
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001222 old_urlopen = urllib.request.urlopen
1223 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001224
James E. Blair3f876d52016-07-22 13:07:14 -07001225 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001226
James E. Blaire1767bc2016-08-02 10:00:27 -07001227 self.launch_server = RecordingLaunchServer(
1228 self.config, self.connections, _run_ansible=self.run_ansible)
1229 self.launch_server.start()
1230 self.history = self.launch_server.build_history
1231 self.builds = self.launch_server.running_builds
1232
1233 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001234 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001235 self.merge_client = zuul.merger.client.MergeClient(
1236 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001237 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001238 self.zk = zuul.zk.ZooKeeper()
1239 self.zk.connect([self.zk_config])
1240
1241 self.fake_nodepool = FakeNodepool(self.zk_config.host,
1242 self.zk_config.port,
1243 self.zk_config.chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001244
James E. Blaire1767bc2016-08-02 10:00:27 -07001245 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001246 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001247 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001248 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001249
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001250 self.webapp = zuul.webapp.WebApp(
1251 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001252 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001253
1254 self.sched.start()
1255 self.sched.reconfigure(self.config)
1256 self.sched.resume()
1257 self.webapp.start()
1258 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001259 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001260
1261 self.addCleanup(self.assertFinalState)
1262 self.addCleanup(self.shutdown)
1263
James E. Blairfef78942016-03-11 16:28:56 -08001264 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001265 # Register connections from the config
1266 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001267
Joshua Hesketh352264b2015-08-11 23:42:08 +10001268 def FakeSMTPFactory(*args, **kw):
1269 args = [self.smtp_messages] + list(args)
1270 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001271
Joshua Hesketh352264b2015-08-11 23:42:08 +10001272 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001273
Joshua Hesketh352264b2015-08-11 23:42:08 +10001274 # Set a changes database so multiple FakeGerrit's can report back to
1275 # a virtual canonical database given by the configured hostname
1276 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001277 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001278
Joshua Hesketh352264b2015-08-11 23:42:08 +10001279 for section_name in self.config.sections():
1280 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1281 section_name, re.I)
1282 if not con_match:
1283 continue
1284 con_name = con_match.group(2)
1285 con_config = dict(self.config.items(section_name))
1286
1287 if 'driver' not in con_config:
1288 raise Exception("No driver specified for connection %s."
1289 % con_name)
1290
1291 con_driver = con_config['driver']
1292
1293 # TODO(jhesketh): load the required class automatically
1294 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001295 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1296 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001297 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001298 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001299 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001300 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001301 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001302 self.event_queues.append(
1303 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001304 setattr(self, 'fake_' + con_name,
1305 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001306 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001307 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001308 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1309 else:
1310 raise Exception("Unknown driver, %s, for connection %s"
1311 % (con_config['driver'], con_name))
1312
1313 # If the [gerrit] or [smtp] sections still exist, load them in as a
1314 # connection named 'gerrit' or 'smtp' respectfully
1315
1316 if 'gerrit' in self.config.sections():
1317 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001318 self.event_queues.append(
1319 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001320 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001321 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001322 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001323
1324 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001325 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001326 zuul.connection.smtp.SMTPConnection(
1327 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001328
James E. Blair83005782015-12-11 14:46:03 -08001329 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001330 # This creates the per-test configuration object. It can be
1331 # overriden by subclasses, but should not need to be since it
1332 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001333 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001334 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001335 if hasattr(self, 'tenant_config_file'):
1336 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001337 git_path = os.path.join(
1338 os.path.dirname(
1339 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1340 'git')
1341 if os.path.exists(git_path):
1342 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001343 project = reponame.replace('_', '/')
1344 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001345 os.path.join(git_path, reponame))
1346
James E. Blair498059b2016-12-20 13:50:13 -08001347 def setupZK(self):
1348 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blairdce6cea2016-12-20 16:45:32 -08001349 self.zk_config = zuul.zk.ZooKeeperConnectionConfig(
1350 self.zk_chroot_fixture.zookeeper_host,
1351 self.zk_chroot_fixture.zookeeper_port,
1352 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001353
James E. Blair96c6bf82016-01-15 16:20:40 -08001354 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001355 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001356
1357 files = {}
1358 for (dirpath, dirnames, filenames) in os.walk(source_path):
1359 for filename in filenames:
1360 test_tree_filepath = os.path.join(dirpath, filename)
1361 common_path = os.path.commonprefix([test_tree_filepath,
1362 source_path])
1363 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1364 with open(test_tree_filepath, 'r') as f:
1365 content = f.read()
1366 files[relative_filepath] = content
1367 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001368 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001369
Clark Boylanb640e052014-04-03 16:41:46 -07001370 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001371 # Make sure that git.Repo objects have been garbage collected.
1372 repos = []
1373 gc.collect()
1374 for obj in gc.get_objects():
1375 if isinstance(obj, git.Repo):
1376 repos.append(obj)
1377 self.assertEqual(len(repos), 0)
1378 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001379 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001380 for tenant in self.sched.abide.tenants.values():
1381 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001382 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001383 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001384
1385 def shutdown(self):
1386 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001387 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001388 self.merge_server.stop()
1389 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001390 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001391 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001392 self.sched.stop()
1393 self.sched.join()
1394 self.statsd.stop()
1395 self.statsd.join()
1396 self.webapp.stop()
1397 self.webapp.join()
1398 self.rpc.stop()
1399 self.rpc.join()
1400 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001401 self.fake_nodepool.stop()
1402 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001403 threads = threading.enumerate()
1404 if len(threads) > 1:
1405 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001406 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001407
1408 def init_repo(self, project):
1409 parts = project.split('/')
1410 path = os.path.join(self.upstream_root, *parts[:-1])
1411 if not os.path.exists(path):
1412 os.makedirs(path)
1413 path = os.path.join(self.upstream_root, project)
1414 repo = git.Repo.init(path)
1415
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001416 with repo.config_writer() as config_writer:
1417 config_writer.set_value('user', 'email', 'user@example.com')
1418 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001419
Clark Boylanb640e052014-04-03 16:41:46 -07001420 repo.index.commit('initial commit')
1421 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001422
James E. Blair97d902e2014-08-21 13:25:56 -07001423 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001424 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001425 repo.git.clean('-x', '-f', '-d')
1426
James E. Blair97d902e2014-08-21 13:25:56 -07001427 def create_branch(self, project, branch):
1428 path = os.path.join(self.upstream_root, project)
1429 repo = git.Repo.init(path)
1430 fn = os.path.join(path, 'README')
1431
1432 branch_head = repo.create_head(branch)
1433 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001434 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001435 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001436 f.close()
1437 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001438 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001439
James E. Blair97d902e2014-08-21 13:25:56 -07001440 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001441 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001442 repo.git.clean('-x', '-f', '-d')
1443
Sachi King9f16d522016-03-16 12:20:45 +11001444 def create_commit(self, project):
1445 path = os.path.join(self.upstream_root, project)
1446 repo = git.Repo(path)
1447 repo.head.reference = repo.heads['master']
1448 file_name = os.path.join(path, 'README')
1449 with open(file_name, 'a') as f:
1450 f.write('creating fake commit\n')
1451 repo.index.add([file_name])
1452 commit = repo.index.commit('Creating a fake commit')
1453 return commit.hexsha
1454
James E. Blairb8c16472015-05-05 14:55:26 -07001455 def orderedRelease(self):
1456 # Run one build at a time to ensure non-race order:
1457 while len(self.builds):
1458 self.release(self.builds[0])
1459 self.waitUntilSettled()
1460
Clark Boylanb640e052014-04-03 16:41:46 -07001461 def release(self, job):
1462 if isinstance(job, FakeBuild):
1463 job.release()
1464 else:
1465 job.waiting = False
1466 self.log.debug("Queued job %s released" % job.unique)
1467 self.gearman_server.wakeConnections()
1468
1469 def getParameter(self, job, name):
1470 if isinstance(job, FakeBuild):
1471 return job.parameters[name]
1472 else:
1473 parameters = json.loads(job.arguments)
1474 return parameters[name]
1475
Clark Boylanb640e052014-04-03 16:41:46 -07001476 def haveAllBuildsReported(self):
1477 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001478 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001479 return False
1480 # Find out if every build that the worker has completed has been
1481 # reported back to Zuul. If it hasn't then that means a Gearman
1482 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001483 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001484 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001485 if not zbuild:
1486 # It has already been reported
1487 continue
1488 # It hasn't been reported yet.
1489 return False
1490 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001491 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001492 if connection.state == 'GRAB_WAIT':
1493 return False
1494 return True
1495
1496 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001497 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001498 for build in builds:
1499 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001500 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001501 for j in conn.related_jobs.values():
1502 if j.unique == build.uuid:
1503 client_job = j
1504 break
1505 if not client_job:
1506 self.log.debug("%s is not known to the gearman client" %
1507 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001508 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001509 if not client_job.handle:
1510 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001511 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001512 server_job = self.gearman_server.jobs.get(client_job.handle)
1513 if not server_job:
1514 self.log.debug("%s is not known to the gearman server" %
1515 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001516 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001517 if not hasattr(server_job, 'waiting'):
1518 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001519 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001520 if server_job.waiting:
1521 continue
James E. Blair17302972016-08-10 16:11:42 -07001522 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001523 self.log.debug("%s has not reported start" % build)
1524 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001525 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001526 if worker_build:
1527 if worker_build.isWaiting():
1528 continue
1529 else:
1530 self.log.debug("%s is running" % worker_build)
1531 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001532 else:
James E. Blair962220f2016-08-03 11:22:38 -07001533 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001534 return False
1535 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001536
James E. Blairdce6cea2016-12-20 16:45:32 -08001537 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001538 if self.fake_nodepool.paused:
1539 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001540 if self.sched.nodepool.requests:
1541 return False
1542 return True
1543
Jan Hruban6b71aff2015-10-22 16:58:08 +02001544 def eventQueuesEmpty(self):
1545 for queue in self.event_queues:
1546 yield queue.empty()
1547
1548 def eventQueuesJoin(self):
1549 for queue in self.event_queues:
1550 queue.join()
1551
Clark Boylanb640e052014-04-03 16:41:46 -07001552 def waitUntilSettled(self):
1553 self.log.debug("Waiting until settled...")
1554 start = time.time()
1555 while True:
1556 if time.time() - start > 10:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001557 self.log.error("Timeout waiting for Zuul to settle")
1558 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001559 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001560 self.log.error(" %s: %s" % (queue, queue.empty()))
1561 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001562 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001563 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001564 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001565 self.log.error("All requests completed: %s" %
1566 (self.areAllNodeRequestsComplete(),))
1567 self.log.error("Merge client jobs: %s" %
1568 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001569 raise Exception("Timeout waiting for Zuul to settle")
1570 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001571
James E. Blaire1767bc2016-08-02 10:00:27 -07001572 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001573 # have all build states propogated to zuul?
1574 if self.haveAllBuildsReported():
1575 # Join ensures that the queue is empty _and_ events have been
1576 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001577 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001578 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001579 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001580 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001581 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001582 self.areAllBuildsWaiting() and
1583 self.areAllNodeRequestsComplete()):
Clark Boylanb640e052014-04-03 16:41:46 -07001584 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001585 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001586 self.log.debug("...settled.")
1587 return
1588 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001589 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001590 self.sched.wake_event.wait(0.1)
1591
1592 def countJobResults(self, jobs, result):
1593 jobs = filter(lambda x: x.result == result, jobs)
1594 return len(jobs)
1595
James E. Blair96c6bf82016-01-15 16:20:40 -08001596 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001597 for job in self.history:
1598 if (job.name == name and
1599 (project is None or
1600 job.parameters['ZUUL_PROJECT'] == project)):
1601 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001602 raise Exception("Unable to find job %s in history" % name)
1603
1604 def assertEmptyQueues(self):
1605 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001606 for tenant in self.sched.abide.tenants.values():
1607 for pipeline in tenant.layout.pipelines.values():
1608 for queue in pipeline.queues:
1609 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001610 print('pipeline %s queue %s contents %s' % (
1611 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001612 self.assertEqual(len(queue.queue), 0,
1613 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001614
1615 def assertReportedStat(self, key, value=None, kind=None):
1616 start = time.time()
1617 while time.time() < (start + 5):
1618 for stat in self.statsd.stats:
1619 pprint.pprint(self.statsd.stats)
1620 k, v = stat.split(':')
1621 if key == k:
1622 if value is None and kind is None:
1623 return
1624 elif value:
1625 if value == v:
1626 return
1627 elif kind:
1628 if v.endswith('|' + kind):
1629 return
1630 time.sleep(0.1)
1631
1632 pprint.pprint(self.statsd.stats)
1633 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001634
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001635 def assertBuilds(self, builds):
1636 """Assert that the running builds are as described.
1637
1638 The list of running builds is examined and must match exactly
1639 the list of builds described by the input.
1640
1641 :arg list builds: A list of dictionaries. Each item in the
1642 list must match the corresponding build in the build
1643 history, and each element of the dictionary must match the
1644 corresponding attribute of the build.
1645
1646 """
James E. Blair3158e282016-08-19 09:34:11 -07001647 try:
1648 self.assertEqual(len(self.builds), len(builds))
1649 for i, d in enumerate(builds):
1650 for k, v in d.items():
1651 self.assertEqual(
1652 getattr(self.builds[i], k), v,
1653 "Element %i in builds does not match" % (i,))
1654 except Exception:
1655 for build in self.builds:
1656 self.log.error("Running build: %s" % build)
1657 else:
1658 self.log.error("No running builds")
1659 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001660
James E. Blairb536ecc2016-08-31 10:11:42 -07001661 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001662 """Assert that the completed builds are as described.
1663
1664 The list of completed builds is examined and must match
1665 exactly the list of builds described by the input.
1666
1667 :arg list history: A list of dictionaries. Each item in the
1668 list must match the corresponding build in the build
1669 history, and each element of the dictionary must match the
1670 corresponding attribute of the build.
1671
James E. Blairb536ecc2016-08-31 10:11:42 -07001672 :arg bool ordered: If true, the history must match the order
1673 supplied, if false, the builds are permitted to have
1674 arrived in any order.
1675
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001676 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001677 def matches(history_item, item):
1678 for k, v in item.items():
1679 if getattr(history_item, k) != v:
1680 return False
1681 return True
James E. Blair3158e282016-08-19 09:34:11 -07001682 try:
1683 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001684 if ordered:
1685 for i, d in enumerate(history):
1686 if not matches(self.history[i], d):
1687 raise Exception(
1688 "Element %i in history does not match" % (i,))
1689 else:
1690 unseen = self.history[:]
1691 for i, d in enumerate(history):
1692 found = False
1693 for unseen_item in unseen:
1694 if matches(unseen_item, d):
1695 found = True
1696 unseen.remove(unseen_item)
1697 break
1698 if not found:
1699 raise Exception("No match found for element %i "
1700 "in history" % (i,))
1701 if unseen:
1702 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001703 except Exception:
1704 for build in self.history:
1705 self.log.error("Completed build: %s" % build)
1706 else:
1707 self.log.error("No completed builds")
1708 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001709
James E. Blair6ac368c2016-12-22 18:07:20 -08001710 def printHistory(self):
1711 """Log the build history.
1712
1713 This can be useful during tests to summarize what jobs have
1714 completed.
1715
1716 """
1717 self.log.debug("Build history:")
1718 for build in self.history:
1719 self.log.debug(build)
1720
James E. Blair59fdbac2015-12-07 17:08:06 -08001721 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001722 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1723
1724 def updateConfigLayout(self, path):
1725 root = os.path.join(self.test_root, "config")
1726 os.makedirs(root)
1727 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1728 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001729- tenant:
1730 name: openstack
1731 source:
1732 gerrit:
1733 config-repos:
1734 - %s
1735 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001736 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001737 self.config.set('zuul', 'tenant_config',
1738 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001739
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001740 def addCommitToRepo(self, project, message, files,
1741 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001742 path = os.path.join(self.upstream_root, project)
1743 repo = git.Repo(path)
1744 repo.head.reference = branch
1745 zuul.merger.merger.reset_repo_to_head(repo)
1746 for fn, content in files.items():
1747 fn = os.path.join(path, fn)
1748 with open(fn, 'w') as f:
1749 f.write(content)
1750 repo.index.add([fn])
1751 commit = repo.index.commit(message)
1752 repo.heads[branch].commit = commit
1753 repo.head.reference = branch
1754 repo.git.clean('-x', '-f', '-d')
1755 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001756 if tag:
1757 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001758
James E. Blair7fc8daa2016-08-08 15:37:15 -07001759 def addEvent(self, connection, event):
1760 """Inject a Fake (Gerrit) event.
1761
1762 This method accepts a JSON-encoded event and simulates Zuul
1763 having received it from Gerrit. It could (and should)
1764 eventually apply to any connection type, but is currently only
1765 used with Gerrit connections. The name of the connection is
1766 used to look up the corresponding server, and the event is
1767 simulated as having been received by all Zuul connections
1768 attached to that server. So if two Gerrit connections in Zuul
1769 are connected to the same Gerrit server, and you invoke this
1770 method specifying the name of one of them, the event will be
1771 received by both.
1772
1773 .. note::
1774
1775 "self.fake_gerrit.addEvent" calls should be migrated to
1776 this method.
1777
1778 :arg str connection: The name of the connection corresponding
1779 to the gerrit server.
1780 :arg str event: The JSON-encoded event.
1781
1782 """
1783 specified_conn = self.connections.connections[connection]
1784 for conn in self.connections.connections.values():
1785 if (isinstance(conn, specified_conn.__class__) and
1786 specified_conn.server == conn.server):
1787 conn.addEvent(event)
1788
James E. Blair3f876d52016-07-22 13:07:14 -07001789
1790class AnsibleZuulTestCase(ZuulTestCase):
1791 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001792 run_ansible = True