blob: 1a66524f807f3548707d0c5acb4bc2548fb2ccee [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
James E. Blair1c236df2017-02-01 14:07:24 -080031from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070032import socket
33import string
34import subprocess
35import swiftclient
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
39import time
Clark Boylanb640e052014-04-03 16:41:46 -070040
41import git
42import gear
43import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080044import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080045import kazoo.exceptions
Clark Boylanb640e052014-04-03 16:41:46 -070046import statsd
47import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080048import testtools.content
49import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080050from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070051
James E. Blaire511d2f2016-12-08 15:22:26 -080052import zuul.driver.gerrit.gerritsource as gerritsource
53import zuul.driver.gerrit.gerritconnection as gerritconnection
Clark Boylanb640e052014-04-03 16:41:46 -070054import zuul.scheduler
55import zuul.webapp
56import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100057import zuul.launcher.server
58import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080060import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070062import zuul.merger.merger
63import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070064import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080065import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070066
67FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
68 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080069
70KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070071
Clark Boylanb640e052014-04-03 16:41:46 -070072
73def repack_repo(path):
74 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
75 output = subprocess.Popen(cmd, close_fds=True,
76 stdout=subprocess.PIPE,
77 stderr=subprocess.PIPE)
78 out = output.communicate()
79 if output.returncode:
80 raise Exception("git repack returned %d" % output.returncode)
81 return out
82
83
84def random_sha1():
85 return hashlib.sha1(str(random.random())).hexdigest()
86
87
James E. Blaira190f3b2015-01-05 14:56:54 -080088def iterate_timeout(max_seconds, purpose):
89 start = time.time()
90 count = 0
91 while (time.time() < start + max_seconds):
92 count += 1
93 yield count
94 time.sleep(0)
95 raise Exception("Timeout waiting for %s" % purpose)
96
97
Clark Boylanb640e052014-04-03 16:41:46 -070098class ChangeReference(git.Reference):
99 _common_path_default = "refs/changes"
100 _points_to_commits_only = True
101
102
103class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700104 categories = {'approved': ('Approved', -1, 1),
105 'code-review': ('Code-Review', -2, 2),
106 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700107
108 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700109 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700110 self.gerrit = gerrit
111 self.reported = 0
112 self.queried = 0
113 self.patchsets = []
114 self.number = number
115 self.project = project
116 self.branch = branch
117 self.subject = subject
118 self.latest_patchset = 0
119 self.depends_on_change = None
120 self.needed_by_changes = []
121 self.fail_merge = False
122 self.messages = []
123 self.data = {
124 'branch': branch,
125 'comments': [],
126 'commitMessage': subject,
127 'createdOn': time.time(),
128 'id': 'I' + random_sha1(),
129 'lastUpdated': time.time(),
130 'number': str(number),
131 'open': status == 'NEW',
132 'owner': {'email': 'user@example.com',
133 'name': 'User Name',
134 'username': 'username'},
135 'patchSets': self.patchsets,
136 'project': project,
137 'status': status,
138 'subject': subject,
139 'submitRecords': [],
140 'url': 'https://hostname/%s' % number}
141
142 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700143 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.data['submitRecords'] = self.getSubmitRecords()
145 self.open = status == 'NEW'
146
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700147 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700148 path = os.path.join(self.upstream_root, self.project)
149 repo = git.Repo(path)
150 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
151 self.latest_patchset),
152 'refs/tags/init')
153 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700154 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700155 repo.git.clean('-x', '-f', '-d')
156
157 path = os.path.join(self.upstream_root, self.project)
158 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700159 for fn, content in files.items():
160 fn = os.path.join(path, fn)
161 with open(fn, 'w') as f:
162 f.write(content)
163 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700164 else:
165 for fni in range(100):
166 fn = os.path.join(path, str(fni))
167 f = open(fn, 'w')
168 for ci in range(4096):
169 f.write(random.choice(string.printable))
170 f.close()
171 repo.index.add([fn])
172
173 r = repo.index.commit(msg)
174 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700175 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700176 repo.git.clean('-x', '-f', '-d')
177 repo.heads['master'].checkout()
178 return r
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700183 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 data = ("test %s %s %s\n" %
185 (self.branch, self.number, self.latest_patchset))
186 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700187 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700188 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 ps_files = [{'file': '/COMMIT_MSG',
190 'type': 'ADDED'},
191 {'file': 'README',
192 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700194 ps_files.append({'file': f, 'type': 'ADDED'})
195 d = {'approvals': [],
196 'createdOn': time.time(),
197 'files': ps_files,
198 'number': str(self.latest_patchset),
199 'ref': 'refs/changes/1/%s/%s' % (self.number,
200 self.latest_patchset),
201 'revision': c.hexsha,
202 'uploader': {'email': 'user@example.com',
203 'name': 'User name',
204 'username': 'user'}}
205 self.data['currentPatchSet'] = d
206 self.patchsets.append(d)
207 self.data['submitRecords'] = self.getSubmitRecords()
208
209 def getPatchsetCreatedEvent(self, patchset):
210 event = {"type": "patchset-created",
211 "change": {"project": self.project,
212 "branch": self.branch,
213 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
214 "number": str(self.number),
215 "subject": self.subject,
216 "owner": {"name": "User Name"},
217 "url": "https://hostname/3"},
218 "patchSet": self.patchsets[patchset - 1],
219 "uploader": {"name": "User Name"}}
220 return event
221
222 def getChangeRestoredEvent(self):
223 event = {"type": "change-restored",
224 "change": {"project": self.project,
225 "branch": self.branch,
226 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
227 "number": str(self.number),
228 "subject": self.subject,
229 "owner": {"name": "User Name"},
230 "url": "https://hostname/3"},
231 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100232 "patchSet": self.patchsets[-1],
233 "reason": ""}
234 return event
235
236 def getChangeAbandonedEvent(self):
237 event = {"type": "change-abandoned",
238 "change": {"project": self.project,
239 "branch": self.branch,
240 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
241 "number": str(self.number),
242 "subject": self.subject,
243 "owner": {"name": "User Name"},
244 "url": "https://hostname/3"},
245 "abandoner": {"name": "User Name"},
246 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700247 "reason": ""}
248 return event
249
250 def getChangeCommentEvent(self, patchset):
251 event = {"type": "comment-added",
252 "change": {"project": self.project,
253 "branch": self.branch,
254 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
255 "number": str(self.number),
256 "subject": self.subject,
257 "owner": {"name": "User Name"},
258 "url": "https://hostname/3"},
259 "patchSet": self.patchsets[patchset - 1],
260 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700261 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700262 "description": "Code-Review",
263 "value": "0"}],
264 "comment": "This is a comment"}
265 return event
266
James E. Blairc2a5ed72017-02-20 14:12:01 -0500267 def getChangeMergedEvent(self):
268 event = {"submitter": {"name": "Jenkins",
269 "username": "jenkins"},
270 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
271 "patchSet": self.patchsets[-1],
272 "change": self.data,
273 "type": "change-merged",
274 "eventCreatedOn": 1487613810}
275 return event
276
Joshua Hesketh642824b2014-07-01 17:54:59 +1000277 def addApproval(self, category, value, username='reviewer_john',
278 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700279 if not granted_on:
280 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000281 approval = {
282 'description': self.categories[category][0],
283 'type': category,
284 'value': str(value),
285 'by': {
286 'username': username,
287 'email': username + '@example.com',
288 },
289 'grantedOn': int(granted_on)
290 }
Clark Boylanb640e052014-04-03 16:41:46 -0700291 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
292 if x['by']['username'] == username and x['type'] == category:
293 del self.patchsets[-1]['approvals'][i]
294 self.patchsets[-1]['approvals'].append(approval)
295 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000296 'author': {'email': 'author@example.com',
297 'name': 'Patchset Author',
298 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700299 'change': {'branch': self.branch,
300 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
301 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000302 'owner': {'email': 'owner@example.com',
303 'name': 'Change Owner',
304 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700305 'project': self.project,
306 'subject': self.subject,
307 'topic': 'master',
308 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000309 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700310 'patchSet': self.patchsets[-1],
311 'type': 'comment-added'}
312 self.data['submitRecords'] = self.getSubmitRecords()
313 return json.loads(json.dumps(event))
314
315 def getSubmitRecords(self):
316 status = {}
317 for cat in self.categories.keys():
318 status[cat] = 0
319
320 for a in self.patchsets[-1]['approvals']:
321 cur = status[a['type']]
322 cat_min, cat_max = self.categories[a['type']][1:]
323 new = int(a['value'])
324 if new == cat_min:
325 cur = new
326 elif abs(new) > abs(cur):
327 cur = new
328 status[a['type']] = cur
329
330 labels = []
331 ok = True
332 for typ, cat in self.categories.items():
333 cur = status[typ]
334 cat_min, cat_max = cat[1:]
335 if cur == cat_min:
336 value = 'REJECT'
337 ok = False
338 elif cur == cat_max:
339 value = 'OK'
340 else:
341 value = 'NEED'
342 ok = False
343 labels.append({'label': cat[0], 'status': value})
344 if ok:
345 return [{'status': 'OK'}]
346 return [{'status': 'NOT_READY',
347 'labels': labels}]
348
349 def setDependsOn(self, other, patchset):
350 self.depends_on_change = other
351 d = {'id': other.data['id'],
352 'number': other.data['number'],
353 'ref': other.patchsets[patchset - 1]['ref']
354 }
355 self.data['dependsOn'] = [d]
356
357 other.needed_by_changes.append(self)
358 needed = other.data.get('neededBy', [])
359 d = {'id': self.data['id'],
360 'number': self.data['number'],
361 'ref': self.patchsets[patchset - 1]['ref'],
362 'revision': self.patchsets[patchset - 1]['revision']
363 }
364 needed.append(d)
365 other.data['neededBy'] = needed
366
367 def query(self):
368 self.queried += 1
369 d = self.data.get('dependsOn')
370 if d:
371 d = d[0]
372 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
373 d['isCurrentPatchSet'] = True
374 else:
375 d['isCurrentPatchSet'] = False
376 return json.loads(json.dumps(self.data))
377
378 def setMerged(self):
379 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000380 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700381 return
382 if self.fail_merge:
383 return
384 self.data['status'] = 'MERGED'
385 self.open = False
386
387 path = os.path.join(self.upstream_root, self.project)
388 repo = git.Repo(path)
389 repo.heads[self.branch].commit = \
390 repo.commit(self.patchsets[-1]['revision'])
391
392 def setReported(self):
393 self.reported += 1
394
395
James E. Blaire511d2f2016-12-08 15:22:26 -0800396class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700397 """A Fake Gerrit connection for use in tests.
398
399 This subclasses
400 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
401 ability for tests to add changes to the fake Gerrit it represents.
402 """
403
Joshua Hesketh352264b2015-08-11 23:42:08 +1000404 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700405
James E. Blaire511d2f2016-12-08 15:22:26 -0800406 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700407 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800408 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000409 connection_config)
410
James E. Blair7fc8daa2016-08-08 15:37:15 -0700411 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700412 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
413 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000414 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700415 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200416 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700417
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700418 def addFakeChange(self, project, branch, subject, status='NEW',
419 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700420 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700421 self.change_number += 1
422 c = FakeChange(self, self.change_number, project, branch, subject,
423 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700424 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700425 self.changes[self.change_number] = c
426 return c
427
Clark Boylanb640e052014-04-03 16:41:46 -0700428 def review(self, project, changeid, message, action):
429 number, ps = changeid.split(',')
430 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000431
432 # Add the approval back onto the change (ie simulate what gerrit would
433 # do).
434 # Usually when zuul leaves a review it'll create a feedback loop where
435 # zuul's review enters another gerrit event (which is then picked up by
436 # zuul). However, we can't mimic this behaviour (by adding this
437 # approval event into the queue) as it stops jobs from checking what
438 # happens before this event is triggered. If a job needs to see what
439 # happens they can add their own verified event into the queue.
440 # Nevertheless, we can update change with the new review in gerrit.
441
James E. Blair8b5408c2016-08-08 15:37:46 -0700442 for cat in action.keys():
443 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000444 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000445
James E. Blair8b5408c2016-08-08 15:37:46 -0700446 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000447 if 'label' in action:
448 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000449 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000450
Clark Boylanb640e052014-04-03 16:41:46 -0700451 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000452
Clark Boylanb640e052014-04-03 16:41:46 -0700453 if 'submit' in action:
454 change.setMerged()
455 if message:
456 change.setReported()
457
458 def query(self, number):
459 change = self.changes.get(int(number))
460 if change:
461 return change.query()
462 return {}
463
James E. Blairc494d542014-08-06 09:23:52 -0700464 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700465 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700466 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800467 if query.startswith('change:'):
468 # Query a specific changeid
469 changeid = query[len('change:'):]
470 l = [change.query() for change in self.changes.values()
471 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700472 elif query.startswith('message:'):
473 # Query the content of a commit message
474 msg = query[len('message:'):].strip()
475 l = [change.query() for change in self.changes.values()
476 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800477 else:
478 # Query all open changes
479 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700480 return l
James E. Blairc494d542014-08-06 09:23:52 -0700481
Joshua Hesketh352264b2015-08-11 23:42:08 +1000482 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700483 pass
484
Joshua Hesketh352264b2015-08-11 23:42:08 +1000485 def getGitUrl(self, project):
486 return os.path.join(self.upstream_root, project.name)
487
Clark Boylanb640e052014-04-03 16:41:46 -0700488
489class BuildHistory(object):
490 def __init__(self, **kw):
491 self.__dict__.update(kw)
492
493 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700494 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
495 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700496
497
498class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200499 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700500 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700501 self.url = url
502
503 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700504 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700505 path = res.path
506 project = '/'.join(path.split('/')[2:-2])
507 ret = '001e# service=git-upload-pack\n'
508 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
509 'multi_ack thin-pack side-band side-band-64k ofs-delta '
510 'shallow no-progress include-tag multi_ack_detailed no-done\n')
511 path = os.path.join(self.upstream_root, project)
512 repo = git.Repo(path)
513 for ref in repo.refs:
514 r = ref.object.hexsha + ' ' + ref.path + '\n'
515 ret += '%04x%s' % (len(r) + 4, r)
516 ret += '0000'
517 return ret
518
519
Clark Boylanb640e052014-04-03 16:41:46 -0700520class FakeStatsd(threading.Thread):
521 def __init__(self):
522 threading.Thread.__init__(self)
523 self.daemon = True
524 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
525 self.sock.bind(('', 0))
526 self.port = self.sock.getsockname()[1]
527 self.wake_read, self.wake_write = os.pipe()
528 self.stats = []
529
530 def run(self):
531 while True:
532 poll = select.poll()
533 poll.register(self.sock, select.POLLIN)
534 poll.register(self.wake_read, select.POLLIN)
535 ret = poll.poll()
536 for (fd, event) in ret:
537 if fd == self.sock.fileno():
538 data = self.sock.recvfrom(1024)
539 if not data:
540 return
541 self.stats.append(data[0])
542 if fd == self.wake_read:
543 return
544
545 def stop(self):
546 os.write(self.wake_write, '1\n')
547
548
James E. Blaire1767bc2016-08-02 10:00:27 -0700549class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700550 log = logging.getLogger("zuul.test")
551
James E. Blair34776ee2016-08-25 13:53:54 -0700552 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700553 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700554 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700555 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700556 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700557 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700558 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700559 # TODOv3(jeblair): self.node is really "the image of the node
560 # assigned". We should rename it (self.node_image?) if we
561 # keep using it like this, or we may end up exposing more of
562 # the complexity around multi-node jobs here
563 # (self.nodes[0].image?)
564 self.node = None
565 if len(self.parameters.get('nodes')) == 1:
566 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700567 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100568 self.pipeline = self.parameters['ZUUL_PIPELINE']
569 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700570 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700571 self.wait_condition = threading.Condition()
572 self.waiting = False
573 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500574 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700575 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700576 self.changes = None
577 if 'ZUUL_CHANGE_IDS' in self.parameters:
578 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700579
James E. Blair3158e282016-08-19 09:34:11 -0700580 def __repr__(self):
581 waiting = ''
582 if self.waiting:
583 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100584 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
585 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700586
Clark Boylanb640e052014-04-03 16:41:46 -0700587 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700588 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700589 self.wait_condition.acquire()
590 self.wait_condition.notify()
591 self.waiting = False
592 self.log.debug("Build %s released" % self.unique)
593 self.wait_condition.release()
594
595 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700596 """Return whether this build is being held.
597
598 :returns: Whether the build is being held.
599 :rtype: bool
600 """
601
Clark Boylanb640e052014-04-03 16:41:46 -0700602 self.wait_condition.acquire()
603 if self.waiting:
604 ret = True
605 else:
606 ret = False
607 self.wait_condition.release()
608 return ret
609
610 def _wait(self):
611 self.wait_condition.acquire()
612 self.waiting = True
613 self.log.debug("Build %s waiting" % self.unique)
614 self.wait_condition.wait()
615 self.wait_condition.release()
616
617 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700618 self.log.debug('Running build %s' % self.unique)
619
James E. Blaire1767bc2016-08-02 10:00:27 -0700620 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700621 self.log.debug('Holding build %s' % self.unique)
622 self._wait()
623 self.log.debug("Build %s continuing" % self.unique)
624
James E. Blair412fba82017-01-26 15:00:50 -0800625 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700626 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800627 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700628 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800629 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500630 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800631 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
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:
Monty Taylord642d852017-02-23 14:05:42 -0500654 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800655 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. Blaira92cbc82017-01-23 14:56:49 -0800686 self._test_root = kw.pop('_test_root', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800687 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700688 self.hold_jobs_in_build = False
689 self.lock = threading.Lock()
690 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700691 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700692 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700693 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800694
James E. Blaira5dba232016-08-08 15:53:24 -0700695 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700696 """Instruct the launcher to report matching builds as failures.
697
698 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700699 :arg Change change: The :py:class:`~tests.base.FakeChange`
700 instance which should cause the job to fail. This job
701 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700702
703 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700704 l = self.fail_tests.get(name, [])
705 l.append(change)
706 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800707
James E. Blair962220f2016-08-03 11:22:38 -0700708 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700709 """Release a held build.
710
711 :arg str regex: A regular expression which, if supplied, will
712 cause only builds with matching names to be released. If
713 not supplied, all builds will be released.
714
715 """
James E. Blair962220f2016-08-03 11:22:38 -0700716 builds = self.running_builds[:]
717 self.log.debug("Releasing build %s (%s)" % (regex,
718 len(self.running_builds)))
719 for build in builds:
720 if not regex or re.match(regex, build.name):
721 self.log.debug("Releasing build %s" %
722 (build.parameters['ZUUL_UUID']))
723 build.release()
724 else:
725 self.log.debug("Not releasing build %s" %
726 (build.parameters['ZUUL_UUID']))
727 self.log.debug("Done releasing builds %s (%s)" %
728 (regex, len(self.running_builds)))
729
James E. Blair17302972016-08-10 16:11:42 -0700730 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700731 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700732 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700733 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700734 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800735 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500736 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800737 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100738 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
739 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700740
741 def stopJob(self, job):
742 self.log.debug("handle stop")
743 parameters = json.loads(job.arguments)
744 uuid = parameters['uuid']
745 for build in self.running_builds:
746 if build.unique == uuid:
747 build.aborted = True
748 build.release()
749 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700750
Joshua Hesketh50c21782016-10-13 21:34:14 +1100751
752class RecordingAnsibleJob(zuul.launcher.server.AnsibleJob):
Paul Belanger96618ed2017-03-01 09:42:33 -0500753 def runPlaybooks(self, args):
Joshua Hesketh50c21782016-10-13 21:34:14 +1100754 build = self.launcher_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800755 build.jobdir = self.jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700756
Paul Belanger96618ed2017-03-01 09:42:33 -0500757 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
James E. Blair412fba82017-01-26 15:00:50 -0800758
Joshua Hesketh50c21782016-10-13 21:34:14 +1100759 self.launcher_server.lock.acquire()
760 self.launcher_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700761 BuildHistory(name=build.name, result=result, changes=build.changes,
762 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800763 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700764 pipeline=build.parameters['ZUUL_PIPELINE'])
765 )
Joshua Hesketh50c21782016-10-13 21:34:14 +1100766 self.launcher_server.running_builds.remove(build)
767 del self.launcher_server.job_builds[self.job.unique]
768 self.launcher_server.lock.release()
James E. Blair412fba82017-01-26 15:00:50 -0800769 return result
770
Monty Taylore6562aa2017-02-20 07:37:39 -0500771 def runAnsible(self, cmd, timeout, trusted=False):
James E. Blair412fba82017-01-26 15:00:50 -0800772 build = self.launcher_server.job_builds[self.job.unique]
773
774 if self.launcher_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600775 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500776 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800777 else:
778 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700779 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800780
James E. Blairad8dca02017-02-21 11:48:32 -0500781 def getHostList(self, args):
782 self.log.debug("hostlist")
783 hosts = super(RecordingAnsibleJob, self).getHostList(args)
784 for name, d in hosts:
785 d['ansible_connection'] = 'local'
786 hosts.append(('localhost', dict(ansible_connection='local')))
787 return hosts
788
James E. Blairf5dbd002015-12-23 15:26:17 -0800789
Clark Boylanb640e052014-04-03 16:41:46 -0700790class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700791 """A Gearman server for use in tests.
792
793 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
794 added to the queue but will not be distributed to workers
795 until released. This attribute may be changed at any time and
796 will take effect for subsequently enqueued jobs, but
797 previously held jobs will still need to be explicitly
798 released.
799
800 """
801
Clark Boylanb640e052014-04-03 16:41:46 -0700802 def __init__(self):
803 self.hold_jobs_in_queue = False
804 super(FakeGearmanServer, self).__init__(0)
805
806 def getJobForConnection(self, connection, peek=False):
807 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
808 for job in queue:
809 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500810 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700811 job.waiting = self.hold_jobs_in_queue
812 else:
813 job.waiting = False
814 if job.waiting:
815 continue
816 if job.name in connection.functions:
817 if not peek:
818 queue.remove(job)
819 connection.related_jobs[job.handle] = job
820 job.worker_connection = connection
821 job.running = True
822 return job
823 return None
824
825 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700826 """Release a held job.
827
828 :arg str regex: A regular expression which, if supplied, will
829 cause only jobs with matching names to be released. If
830 not supplied, all jobs will be released.
831 """
Clark Boylanb640e052014-04-03 16:41:46 -0700832 released = False
833 qlen = (len(self.high_queue) + len(self.normal_queue) +
834 len(self.low_queue))
835 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
836 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500837 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700838 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500839 parameters = json.loads(job.arguments)
840 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700841 self.log.debug("releasing queued job %s" %
842 job.unique)
843 job.waiting = False
844 released = True
845 else:
846 self.log.debug("not releasing queued job %s" %
847 job.unique)
848 if released:
849 self.wakeConnections()
850 qlen = (len(self.high_queue) + len(self.normal_queue) +
851 len(self.low_queue))
852 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
853
854
855class FakeSMTP(object):
856 log = logging.getLogger('zuul.FakeSMTP')
857
858 def __init__(self, messages, server, port):
859 self.server = server
860 self.port = port
861 self.messages = messages
862
863 def sendmail(self, from_email, to_email, msg):
864 self.log.info("Sending email from %s, to %s, with msg %s" % (
865 from_email, to_email, msg))
866
867 headers = msg.split('\n\n', 1)[0]
868 body = msg.split('\n\n', 1)[1]
869
870 self.messages.append(dict(
871 from_email=from_email,
872 to_email=to_email,
873 msg=msg,
874 headers=headers,
875 body=body,
876 ))
877
878 return True
879
880 def quit(self):
881 return True
882
883
884class FakeSwiftClientConnection(swiftclient.client.Connection):
885 def post_account(self, headers):
886 # Do nothing
887 pass
888
889 def get_auth(self):
890 # Returns endpoint and (unused) auth token
891 endpoint = os.path.join('https://storage.example.org', 'V1',
892 'AUTH_account')
893 return endpoint, ''
894
895
James E. Blairdce6cea2016-12-20 16:45:32 -0800896class FakeNodepool(object):
897 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800898 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800899
900 log = logging.getLogger("zuul.test.FakeNodepool")
901
902 def __init__(self, host, port, chroot):
903 self.client = kazoo.client.KazooClient(
904 hosts='%s:%s%s' % (host, port, chroot))
905 self.client.start()
906 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800907 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800908 self.thread = threading.Thread(target=self.run)
909 self.thread.daemon = True
910 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800911 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800912
913 def stop(self):
914 self._running = False
915 self.thread.join()
916 self.client.stop()
917 self.client.close()
918
919 def run(self):
920 while self._running:
921 self._run()
922 time.sleep(0.1)
923
924 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800925 if self.paused:
926 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800927 for req in self.getNodeRequests():
928 self.fulfillRequest(req)
929
930 def getNodeRequests(self):
931 try:
932 reqids = self.client.get_children(self.REQUEST_ROOT)
933 except kazoo.exceptions.NoNodeError:
934 return []
935 reqs = []
936 for oid in sorted(reqids):
937 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800938 try:
939 data, stat = self.client.get(path)
940 data = json.loads(data)
941 data['_oid'] = oid
942 reqs.append(data)
943 except kazoo.exceptions.NoNodeError:
944 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800945 return reqs
946
James E. Blaire18d4602017-01-05 11:17:28 -0800947 def getNodes(self):
948 try:
949 nodeids = self.client.get_children(self.NODE_ROOT)
950 except kazoo.exceptions.NoNodeError:
951 return []
952 nodes = []
953 for oid in sorted(nodeids):
954 path = self.NODE_ROOT + '/' + oid
955 data, stat = self.client.get(path)
956 data = json.loads(data)
957 data['_oid'] = oid
958 try:
959 lockfiles = self.client.get_children(path + '/lock')
960 except kazoo.exceptions.NoNodeError:
961 lockfiles = []
962 if lockfiles:
963 data['_lock'] = True
964 else:
965 data['_lock'] = False
966 nodes.append(data)
967 return nodes
968
James E. Blaira38c28e2017-01-04 10:33:20 -0800969 def makeNode(self, request_id, node_type):
970 now = time.time()
971 path = '/nodepool/nodes/'
972 data = dict(type=node_type,
973 provider='test-provider',
974 region='test-region',
975 az=None,
976 public_ipv4='127.0.0.1',
977 private_ipv4=None,
978 public_ipv6=None,
979 allocated_to=request_id,
980 state='ready',
981 state_time=now,
982 created_time=now,
983 updated_time=now,
984 image_id=None,
985 launcher='fake-nodepool')
986 data = json.dumps(data)
987 path = self.client.create(path, data,
988 makepath=True,
989 sequence=True)
990 nodeid = path.split("/")[-1]
991 return nodeid
992
James E. Blair6ab79e02017-01-06 10:10:17 -0800993 def addFailRequest(self, request):
994 self.fail_requests.add(request['_oid'])
995
James E. Blairdce6cea2016-12-20 16:45:32 -0800996 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -0800997 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -0800998 return
999 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001000 oid = request['_oid']
1001 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001002
James E. Blair6ab79e02017-01-06 10:10:17 -08001003 if oid in self.fail_requests:
1004 request['state'] = 'failed'
1005 else:
1006 request['state'] = 'fulfilled'
1007 nodes = []
1008 for node in request['node_types']:
1009 nodeid = self.makeNode(oid, node)
1010 nodes.append(nodeid)
1011 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001012
James E. Blaira38c28e2017-01-04 10:33:20 -08001013 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001014 path = self.REQUEST_ROOT + '/' + oid
1015 data = json.dumps(request)
1016 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1017 self.client.set(path, data)
1018
1019
James E. Blair498059b2016-12-20 13:50:13 -08001020class ChrootedKazooFixture(fixtures.Fixture):
1021 def __init__(self):
1022 super(ChrootedKazooFixture, self).__init__()
1023
1024 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1025 if ':' in zk_host:
1026 host, port = zk_host.split(':')
1027 else:
1028 host = zk_host
1029 port = None
1030
1031 self.zookeeper_host = host
1032
1033 if not port:
1034 self.zookeeper_port = 2181
1035 else:
1036 self.zookeeper_port = int(port)
1037
1038 def _setUp(self):
1039 # Make sure the test chroot paths do not conflict
1040 random_bits = ''.join(random.choice(string.ascii_lowercase +
1041 string.ascii_uppercase)
1042 for x in range(8))
1043
1044 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1045 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1046
1047 # Ensure the chroot path exists and clean up any pre-existing znodes.
1048 _tmp_client = kazoo.client.KazooClient(
1049 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1050 _tmp_client.start()
1051
1052 if _tmp_client.exists(self.zookeeper_chroot):
1053 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1054
1055 _tmp_client.ensure_path(self.zookeeper_chroot)
1056 _tmp_client.stop()
1057 _tmp_client.close()
1058
1059 self.addCleanup(self._cleanup)
1060
1061 def _cleanup(self):
1062 '''Remove the chroot path.'''
1063 # Need a non-chroot'ed client to remove the chroot path
1064 _tmp_client = kazoo.client.KazooClient(
1065 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1066 _tmp_client.start()
1067 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1068 _tmp_client.stop()
1069
1070
Maru Newby3fe5f852015-01-13 04:22:14 +00001071class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001072 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001073 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001074
James E. Blair1c236df2017-02-01 14:07:24 -08001075 def attachLogs(self, *args):
1076 def reader():
1077 self._log_stream.seek(0)
1078 while True:
1079 x = self._log_stream.read(4096)
1080 if not x:
1081 break
1082 yield x.encode('utf8')
1083 content = testtools.content.content_from_reader(
1084 reader,
1085 testtools.content_type.UTF8_TEXT,
1086 False)
1087 self.addDetail('logging', content)
1088
Clark Boylanb640e052014-04-03 16:41:46 -07001089 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001090 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001091 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1092 try:
1093 test_timeout = int(test_timeout)
1094 except ValueError:
1095 # If timeout value is invalid do not set a timeout.
1096 test_timeout = 0
1097 if test_timeout > 0:
1098 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1099
1100 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1101 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1102 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1103 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1104 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1105 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1106 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1107 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1108 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1109 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001110 self._log_stream = StringIO()
1111 self.addOnException(self.attachLogs)
1112 else:
1113 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001114
James E. Blair1c236df2017-02-01 14:07:24 -08001115 handler = logging.StreamHandler(self._log_stream)
1116 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1117 '%(levelname)-8s %(message)s')
1118 handler.setFormatter(formatter)
1119
1120 logger = logging.getLogger()
1121 logger.setLevel(logging.DEBUG)
1122 logger.addHandler(handler)
1123
1124 # NOTE(notmorgan): Extract logging overrides for specific
1125 # libraries from the OS_LOG_DEFAULTS env and create loggers
1126 # for each. This is used to limit the output during test runs
1127 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001128 log_defaults_from_env = os.environ.get(
1129 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001130 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001131
James E. Blairdce6cea2016-12-20 16:45:32 -08001132 if log_defaults_from_env:
1133 for default in log_defaults_from_env.split(','):
1134 try:
1135 name, level_str = default.split('=', 1)
1136 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001137 logger = logging.getLogger(name)
1138 logger.setLevel(level)
1139 logger.addHandler(handler)
1140 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001141 except ValueError:
1142 # NOTE(notmorgan): Invalid format of the log default,
1143 # skip and don't try and apply a logger for the
1144 # specified module
1145 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001146
Maru Newby3fe5f852015-01-13 04:22:14 +00001147
1148class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001149 """A test case with a functioning Zuul.
1150
1151 The following class variables are used during test setup and can
1152 be overidden by subclasses but are effectively read-only once a
1153 test method starts running:
1154
1155 :cvar str config_file: This points to the main zuul config file
1156 within the fixtures directory. Subclasses may override this
1157 to obtain a different behavior.
1158
1159 :cvar str tenant_config_file: This is the tenant config file
1160 (which specifies from what git repos the configuration should
1161 be loaded). It defaults to the value specified in
1162 `config_file` but can be overidden by subclasses to obtain a
1163 different tenant/project layout while using the standard main
1164 configuration.
1165
1166 The following are instance variables that are useful within test
1167 methods:
1168
1169 :ivar FakeGerritConnection fake_<connection>:
1170 A :py:class:`~tests.base.FakeGerritConnection` will be
1171 instantiated for each connection present in the config file
1172 and stored here. For instance, `fake_gerrit` will hold the
1173 FakeGerritConnection object for a connection named `gerrit`.
1174
1175 :ivar FakeGearmanServer gearman_server: An instance of
1176 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1177 server that all of the Zuul components in this test use to
1178 communicate with each other.
1179
1180 :ivar RecordingLaunchServer launch_server: An instance of
1181 :py:class:`~tests.base.RecordingLaunchServer` which is the
1182 Ansible launch server used to run jobs for this test.
1183
1184 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1185 representing currently running builds. They are appended to
1186 the list in the order they are launched, and removed from this
1187 list upon completion.
1188
1189 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1190 objects representing completed builds. They are appended to
1191 the list in the order they complete.
1192
1193 """
1194
James E. Blair83005782015-12-11 14:46:03 -08001195 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001196 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001197
1198 def _startMerger(self):
1199 self.merge_server = zuul.merger.server.MergeServer(self.config,
1200 self.connections)
1201 self.merge_server.start()
1202
Maru Newby3fe5f852015-01-13 04:22:14 +00001203 def setUp(self):
1204 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001205
1206 self.setupZK()
1207
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001208 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001209 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001210 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1211 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001212 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001213 tmp_root = tempfile.mkdtemp(
1214 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001215 self.test_root = os.path.join(tmp_root, "zuul-test")
1216 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001217 self.merger_src_root = os.path.join(self.test_root, "merger-git")
1218 self.launcher_src_root = os.path.join(self.test_root, "launcher-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001219 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001220
1221 if os.path.exists(self.test_root):
1222 shutil.rmtree(self.test_root)
1223 os.makedirs(self.test_root)
1224 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001225 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001226
1227 # Make per test copy of Configuration.
1228 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001229 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001230 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001231 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001232 self.config.set('merger', 'git_dir', self.merger_src_root)
1233 self.config.set('launcher', 'git_dir', self.launcher_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001234 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001235
1236 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001237 # TODOv3(jeblair): remove these and replace with new git
1238 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001239 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001240 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001241 self.init_repo("org/project5")
1242 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001243 self.init_repo("org/one-job-project")
1244 self.init_repo("org/nonvoting-project")
1245 self.init_repo("org/templated-project")
1246 self.init_repo("org/layered-project")
1247 self.init_repo("org/node-project")
1248 self.init_repo("org/conflict-project")
1249 self.init_repo("org/noop-project")
1250 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001251 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001252
1253 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001254 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1255 # see: https://github.com/jsocol/pystatsd/issues/61
1256 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001257 os.environ['STATSD_PORT'] = str(self.statsd.port)
1258 self.statsd.start()
1259 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001260 reload_module(statsd)
1261 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001262
1263 self.gearman_server = FakeGearmanServer()
1264
1265 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001266 self.log.info("Gearman server on port %s" %
1267 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001268
James E. Blaire511d2f2016-12-08 15:22:26 -08001269 gerritsource.GerritSource.replication_timeout = 1.5
1270 gerritsource.GerritSource.replication_retry_interval = 0.5
1271 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001272
Joshua Hesketh352264b2015-08-11 23:42:08 +10001273 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001274
1275 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1276 FakeSwiftClientConnection))
James E. Blaire511d2f2016-12-08 15:22:26 -08001277
Clark Boylanb640e052014-04-03 16:41:46 -07001278 self.swift = zuul.lib.swift.Swift(self.config)
1279
Jan Hruban6b71aff2015-10-22 16:58:08 +02001280 self.event_queues = [
1281 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001282 self.sched.trigger_event_queue,
1283 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001284 ]
1285
James E. Blairfef78942016-03-11 16:28:56 -08001286 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001287 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001288
Clark Boylanb640e052014-04-03 16:41:46 -07001289 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001290 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001291 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001292 return FakeURLOpener(self.upstream_root, *args, **kw)
1293
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001294 old_urlopen = urllib.request.urlopen
1295 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001296
James E. Blair3f876d52016-07-22 13:07:14 -07001297 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001298
James E. Blaire1767bc2016-08-02 10:00:27 -07001299 self.launch_server = RecordingLaunchServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001300 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001301 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001302 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001303 _test_root=self.test_root,
1304 keep_jobdir=KEEP_TEMPDIRS)
James E. Blaire1767bc2016-08-02 10:00:27 -07001305 self.launch_server.start()
1306 self.history = self.launch_server.build_history
1307 self.builds = self.launch_server.running_builds
1308
1309 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001310 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001311 self.merge_client = zuul.merger.client.MergeClient(
1312 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001313 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001314 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001315 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001316
James E. Blair0d5a36e2017-02-21 10:53:44 -05001317 self.fake_nodepool = FakeNodepool(
1318 self.zk_chroot_fixture.zookeeper_host,
1319 self.zk_chroot_fixture.zookeeper_port,
1320 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001321
James E. Blaire1767bc2016-08-02 10:00:27 -07001322 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001323 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001324 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001325 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001326
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001327 self.webapp = zuul.webapp.WebApp(
1328 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001329 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001330
1331 self.sched.start()
1332 self.sched.reconfigure(self.config)
1333 self.sched.resume()
1334 self.webapp.start()
1335 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001336 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001337
Clark Boylanb640e052014-04-03 16:41:46 -07001338 self.addCleanup(self.shutdown)
1339
James E. Blaire18d4602017-01-05 11:17:28 -08001340 def tearDown(self):
1341 super(ZuulTestCase, self).tearDown()
1342 self.assertFinalState()
1343
James E. Blairfef78942016-03-11 16:28:56 -08001344 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001345 # Set up gerrit related fakes
1346 # Set a changes database so multiple FakeGerrit's can report back to
1347 # a virtual canonical database given by the configured hostname
1348 self.gerrit_changes_dbs = {}
1349
1350 def getGerritConnection(driver, name, config):
1351 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1352 con = FakeGerritConnection(driver, name, config,
1353 changes_db=db,
1354 upstream_root=self.upstream_root)
1355 self.event_queues.append(con.event_queue)
1356 setattr(self, 'fake_' + name, con)
1357 return con
1358
1359 self.useFixture(fixtures.MonkeyPatch(
1360 'zuul.driver.gerrit.GerritDriver.getConnection',
1361 getGerritConnection))
1362
1363 # Set up smtp related fakes
Joshua Hesketh352264b2015-08-11 23:42:08 +10001364 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001365
Joshua Hesketh352264b2015-08-11 23:42:08 +10001366 def FakeSMTPFactory(*args, **kw):
1367 args = [self.smtp_messages] + list(args)
1368 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001369
Joshua Hesketh352264b2015-08-11 23:42:08 +10001370 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001371
James E. Blaire511d2f2016-12-08 15:22:26 -08001372 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001373 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001374 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001375
James E. Blair83005782015-12-11 14:46:03 -08001376 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001377 # This creates the per-test configuration object. It can be
1378 # overriden by subclasses, but should not need to be since it
1379 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001380 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001381 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001382 if hasattr(self, 'tenant_config_file'):
1383 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001384 git_path = os.path.join(
1385 os.path.dirname(
1386 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1387 'git')
1388 if os.path.exists(git_path):
1389 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001390 project = reponame.replace('_', '/')
1391 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001392 os.path.join(git_path, reponame))
1393
James E. Blair498059b2016-12-20 13:50:13 -08001394 def setupZK(self):
1395 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001396 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001397 self.zk_chroot_fixture.zookeeper_host,
1398 self.zk_chroot_fixture.zookeeper_port,
1399 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001400
James E. Blair96c6bf82016-01-15 16:20:40 -08001401 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001402 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001403
1404 files = {}
1405 for (dirpath, dirnames, filenames) in os.walk(source_path):
1406 for filename in filenames:
1407 test_tree_filepath = os.path.join(dirpath, filename)
1408 common_path = os.path.commonprefix([test_tree_filepath,
1409 source_path])
1410 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1411 with open(test_tree_filepath, 'r') as f:
1412 content = f.read()
1413 files[relative_filepath] = content
1414 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001415 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001416
James E. Blaire18d4602017-01-05 11:17:28 -08001417 def assertNodepoolState(self):
1418 # Make sure that there are no pending requests
1419
1420 requests = self.fake_nodepool.getNodeRequests()
1421 self.assertEqual(len(requests), 0)
1422
1423 nodes = self.fake_nodepool.getNodes()
1424 for node in nodes:
1425 self.assertFalse(node['_lock'], "Node %s is locked" %
1426 (node['_oid'],))
1427
Clark Boylanb640e052014-04-03 16:41:46 -07001428 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001429 # Make sure that git.Repo objects have been garbage collected.
1430 repos = []
1431 gc.collect()
1432 for obj in gc.get_objects():
1433 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001434 self.log.debug("Leaked git repo object: %s" % repr(obj))
1435 for r in gc.get_referrers(obj):
1436 self.log.debug(" referrer: %s" % repr(r))
Clark Boylanb640e052014-04-03 16:41:46 -07001437 repos.append(obj)
1438 self.assertEqual(len(repos), 0)
1439 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001440 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001441 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001442 for tenant in self.sched.abide.tenants.values():
1443 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001444 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001445 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001446
1447 def shutdown(self):
1448 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001449 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001450 self.merge_server.stop()
1451 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001452 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001453 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001454 self.sched.stop()
1455 self.sched.join()
1456 self.statsd.stop()
1457 self.statsd.join()
1458 self.webapp.stop()
1459 self.webapp.join()
1460 self.rpc.stop()
1461 self.rpc.join()
1462 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001463 self.fake_nodepool.stop()
1464 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001465 threads = threading.enumerate()
1466 if len(threads) > 1:
1467 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001468 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001469
1470 def init_repo(self, project):
1471 parts = project.split('/')
1472 path = os.path.join(self.upstream_root, *parts[:-1])
1473 if not os.path.exists(path):
1474 os.makedirs(path)
1475 path = os.path.join(self.upstream_root, project)
1476 repo = git.Repo.init(path)
1477
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001478 with repo.config_writer() as config_writer:
1479 config_writer.set_value('user', 'email', 'user@example.com')
1480 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001481
Clark Boylanb640e052014-04-03 16:41:46 -07001482 repo.index.commit('initial commit')
1483 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001484
James E. Blair97d902e2014-08-21 13:25:56 -07001485 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001486 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001487 repo.git.clean('-x', '-f', '-d')
1488
James E. Blair97d902e2014-08-21 13:25:56 -07001489 def create_branch(self, project, branch):
1490 path = os.path.join(self.upstream_root, project)
1491 repo = git.Repo.init(path)
1492 fn = os.path.join(path, 'README')
1493
1494 branch_head = repo.create_head(branch)
1495 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001496 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001497 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001498 f.close()
1499 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001500 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001501
James E. Blair97d902e2014-08-21 13:25:56 -07001502 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001503 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001504 repo.git.clean('-x', '-f', '-d')
1505
Sachi King9f16d522016-03-16 12:20:45 +11001506 def create_commit(self, project):
1507 path = os.path.join(self.upstream_root, project)
1508 repo = git.Repo(path)
1509 repo.head.reference = repo.heads['master']
1510 file_name = os.path.join(path, 'README')
1511 with open(file_name, 'a') as f:
1512 f.write('creating fake commit\n')
1513 repo.index.add([file_name])
1514 commit = repo.index.commit('Creating a fake commit')
1515 return commit.hexsha
1516
James E. Blairb8c16472015-05-05 14:55:26 -07001517 def orderedRelease(self):
1518 # Run one build at a time to ensure non-race order:
1519 while len(self.builds):
1520 self.release(self.builds[0])
1521 self.waitUntilSettled()
1522
Clark Boylanb640e052014-04-03 16:41:46 -07001523 def release(self, job):
1524 if isinstance(job, FakeBuild):
1525 job.release()
1526 else:
1527 job.waiting = False
1528 self.log.debug("Queued job %s released" % job.unique)
1529 self.gearman_server.wakeConnections()
1530
1531 def getParameter(self, job, name):
1532 if isinstance(job, FakeBuild):
1533 return job.parameters[name]
1534 else:
1535 parameters = json.loads(job.arguments)
1536 return parameters[name]
1537
Clark Boylanb640e052014-04-03 16:41:46 -07001538 def haveAllBuildsReported(self):
1539 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001540 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001541 return False
1542 # Find out if every build that the worker has completed has been
1543 # reported back to Zuul. If it hasn't then that means a Gearman
1544 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001545 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001546 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001547 if not zbuild:
1548 # It has already been reported
1549 continue
1550 # It hasn't been reported yet.
1551 return False
1552 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001553 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001554 if connection.state == 'GRAB_WAIT':
1555 return False
1556 return True
1557
1558 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001559 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001560 for build in builds:
1561 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001562 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001563 for j in conn.related_jobs.values():
1564 if j.unique == build.uuid:
1565 client_job = j
1566 break
1567 if not client_job:
1568 self.log.debug("%s is not known to the gearman client" %
1569 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001570 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001571 if not client_job.handle:
1572 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001573 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001574 server_job = self.gearman_server.jobs.get(client_job.handle)
1575 if not server_job:
1576 self.log.debug("%s is not known to the gearman server" %
1577 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001578 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001579 if not hasattr(server_job, 'waiting'):
1580 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001581 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001582 if server_job.waiting:
1583 continue
James E. Blair17302972016-08-10 16:11:42 -07001584 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001585 self.log.debug("%s has not reported start" % build)
1586 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001587 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001588 if worker_build:
1589 if worker_build.isWaiting():
1590 continue
1591 else:
1592 self.log.debug("%s is running" % worker_build)
1593 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001594 else:
James E. Blair962220f2016-08-03 11:22:38 -07001595 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001596 return False
1597 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001598
James E. Blairdce6cea2016-12-20 16:45:32 -08001599 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001600 if self.fake_nodepool.paused:
1601 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001602 if self.sched.nodepool.requests:
1603 return False
1604 return True
1605
Jan Hruban6b71aff2015-10-22 16:58:08 +02001606 def eventQueuesEmpty(self):
1607 for queue in self.event_queues:
1608 yield queue.empty()
1609
1610 def eventQueuesJoin(self):
1611 for queue in self.event_queues:
1612 queue.join()
1613
Clark Boylanb640e052014-04-03 16:41:46 -07001614 def waitUntilSettled(self):
1615 self.log.debug("Waiting until settled...")
1616 start = time.time()
1617 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001618 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001619 self.log.error("Timeout waiting for Zuul to settle")
1620 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001621 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001622 self.log.error(" %s: %s" % (queue, queue.empty()))
1623 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001624 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001625 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001626 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001627 self.log.error("All requests completed: %s" %
1628 (self.areAllNodeRequestsComplete(),))
1629 self.log.error("Merge client jobs: %s" %
1630 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001631 raise Exception("Timeout waiting for Zuul to settle")
1632 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001633
James E. Blaire1767bc2016-08-02 10:00:27 -07001634 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001635 # have all build states propogated to zuul?
1636 if self.haveAllBuildsReported():
1637 # Join ensures that the queue is empty _and_ events have been
1638 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001639 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001640 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001641 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001642 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001643 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001644 self.areAllNodeRequestsComplete() and
1645 all(self.eventQueuesEmpty())):
1646 # The queue empty check is placed at the end to
1647 # ensure that if a component adds an event between
1648 # when locked the run handler and checked that the
1649 # components were stable, we don't erroneously
1650 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001651 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001652 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001653 self.log.debug("...settled.")
1654 return
1655 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001656 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001657 self.sched.wake_event.wait(0.1)
1658
1659 def countJobResults(self, jobs, result):
1660 jobs = filter(lambda x: x.result == result, jobs)
1661 return len(jobs)
1662
James E. Blair96c6bf82016-01-15 16:20:40 -08001663 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001664 for job in self.history:
1665 if (job.name == name and
1666 (project is None or
1667 job.parameters['ZUUL_PROJECT'] == project)):
1668 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001669 raise Exception("Unable to find job %s in history" % name)
1670
1671 def assertEmptyQueues(self):
1672 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001673 for tenant in self.sched.abide.tenants.values():
1674 for pipeline in tenant.layout.pipelines.values():
1675 for queue in pipeline.queues:
1676 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001677 print('pipeline %s queue %s contents %s' % (
1678 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001679 self.assertEqual(len(queue.queue), 0,
1680 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001681
1682 def assertReportedStat(self, key, value=None, kind=None):
1683 start = time.time()
1684 while time.time() < (start + 5):
1685 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001686 k, v = stat.split(':')
1687 if key == k:
1688 if value is None and kind is None:
1689 return
1690 elif value:
1691 if value == v:
1692 return
1693 elif kind:
1694 if v.endswith('|' + kind):
1695 return
1696 time.sleep(0.1)
1697
Clark Boylanb640e052014-04-03 16:41:46 -07001698 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001699
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001700 def assertBuilds(self, builds):
1701 """Assert that the running builds are as described.
1702
1703 The list of running builds is examined and must match exactly
1704 the list of builds described by the input.
1705
1706 :arg list builds: A list of dictionaries. Each item in the
1707 list must match the corresponding build in the build
1708 history, and each element of the dictionary must match the
1709 corresponding attribute of the build.
1710
1711 """
James E. Blair3158e282016-08-19 09:34:11 -07001712 try:
1713 self.assertEqual(len(self.builds), len(builds))
1714 for i, d in enumerate(builds):
1715 for k, v in d.items():
1716 self.assertEqual(
1717 getattr(self.builds[i], k), v,
1718 "Element %i in builds does not match" % (i,))
1719 except Exception:
1720 for build in self.builds:
1721 self.log.error("Running build: %s" % build)
1722 else:
1723 self.log.error("No running builds")
1724 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001725
James E. Blairb536ecc2016-08-31 10:11:42 -07001726 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001727 """Assert that the completed builds are as described.
1728
1729 The list of completed builds is examined and must match
1730 exactly the list of builds described by the input.
1731
1732 :arg list history: A list of dictionaries. Each item in the
1733 list must match the corresponding build in the build
1734 history, and each element of the dictionary must match the
1735 corresponding attribute of the build.
1736
James E. Blairb536ecc2016-08-31 10:11:42 -07001737 :arg bool ordered: If true, the history must match the order
1738 supplied, if false, the builds are permitted to have
1739 arrived in any order.
1740
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001741 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001742 def matches(history_item, item):
1743 for k, v in item.items():
1744 if getattr(history_item, k) != v:
1745 return False
1746 return True
James E. Blair3158e282016-08-19 09:34:11 -07001747 try:
1748 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001749 if ordered:
1750 for i, d in enumerate(history):
1751 if not matches(self.history[i], d):
1752 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001753 "Element %i in history does not match %s" %
1754 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001755 else:
1756 unseen = self.history[:]
1757 for i, d in enumerate(history):
1758 found = False
1759 for unseen_item in unseen:
1760 if matches(unseen_item, d):
1761 found = True
1762 unseen.remove(unseen_item)
1763 break
1764 if not found:
1765 raise Exception("No match found for element %i "
1766 "in history" % (i,))
1767 if unseen:
1768 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001769 except Exception:
1770 for build in self.history:
1771 self.log.error("Completed build: %s" % build)
1772 else:
1773 self.log.error("No completed builds")
1774 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001775
James E. Blair6ac368c2016-12-22 18:07:20 -08001776 def printHistory(self):
1777 """Log the build history.
1778
1779 This can be useful during tests to summarize what jobs have
1780 completed.
1781
1782 """
1783 self.log.debug("Build history:")
1784 for build in self.history:
1785 self.log.debug(build)
1786
James E. Blair59fdbac2015-12-07 17:08:06 -08001787 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001788 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1789
1790 def updateConfigLayout(self, path):
1791 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001792 if not os.path.exists(root):
1793 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001794 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1795 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001796- tenant:
1797 name: openstack
1798 source:
1799 gerrit:
1800 config-repos:
1801 - %s
1802 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001803 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001804 self.config.set('zuul', 'tenant_config',
1805 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001806
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001807 def addCommitToRepo(self, project, message, files,
1808 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001809 path = os.path.join(self.upstream_root, project)
1810 repo = git.Repo(path)
1811 repo.head.reference = branch
1812 zuul.merger.merger.reset_repo_to_head(repo)
1813 for fn, content in files.items():
1814 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001815 try:
1816 os.makedirs(os.path.dirname(fn))
1817 except OSError:
1818 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001819 with open(fn, 'w') as f:
1820 f.write(content)
1821 repo.index.add([fn])
1822 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001823 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001824 repo.heads[branch].commit = commit
1825 repo.head.reference = branch
1826 repo.git.clean('-x', '-f', '-d')
1827 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001828 if tag:
1829 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001830 return before
1831
1832 def commitLayoutUpdate(self, orig_name, source_name):
1833 source_path = os.path.join(self.test_root, 'upstream',
1834 source_name, 'zuul.yaml')
1835 with open(source_path, 'r') as nt:
1836 before = self.addCommitToRepo(
1837 orig_name, 'Pulling content from %s' % source_name,
1838 {'zuul.yaml': nt.read()})
1839 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001840
James E. Blair7fc8daa2016-08-08 15:37:15 -07001841 def addEvent(self, connection, event):
1842 """Inject a Fake (Gerrit) event.
1843
1844 This method accepts a JSON-encoded event and simulates Zuul
1845 having received it from Gerrit. It could (and should)
1846 eventually apply to any connection type, but is currently only
1847 used with Gerrit connections. The name of the connection is
1848 used to look up the corresponding server, and the event is
1849 simulated as having been received by all Zuul connections
1850 attached to that server. So if two Gerrit connections in Zuul
1851 are connected to the same Gerrit server, and you invoke this
1852 method specifying the name of one of them, the event will be
1853 received by both.
1854
1855 .. note::
1856
1857 "self.fake_gerrit.addEvent" calls should be migrated to
1858 this method.
1859
1860 :arg str connection: The name of the connection corresponding
1861 to the gerrit server.
1862 :arg str event: The JSON-encoded event.
1863
1864 """
1865 specified_conn = self.connections.connections[connection]
1866 for conn in self.connections.connections.values():
1867 if (isinstance(conn, specified_conn.__class__) and
1868 specified_conn.server == conn.server):
1869 conn.addEvent(event)
1870
James E. Blair3f876d52016-07-22 13:07:14 -07001871
1872class AnsibleZuulTestCase(ZuulTestCase):
1873 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001874 run_ansible = True