blob: f26863ece1c657023a1e4fe8e511bc2d1613f797 [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
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
38import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060039import uuid
40
Clark Boylanb640e052014-04-03 16:41:46 -070041
42import git
43import gear
44import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080045import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080046import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060047import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070048import statsd
49import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070053
James E. Blaire511d2f2016-12-08 15:22:26 -080054import zuul.driver.gerrit.gerritsource as gerritsource
55import zuul.driver.gerrit.gerritconnection as gerritconnection
Joshua Heskethd78b4482015-09-14 16:56:34 -060056import zuul.connection.sql
Clark Boylanb640e052014-04-03 16:41:46 -070057import zuul.scheduler
58import zuul.webapp
59import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100060import zuul.launcher.server
61import zuul.launcher.client
James E. Blair83005782015-12-11 14:46:03 -080062import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070064import zuul.merger.merger
65import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070066import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080067import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070068
69FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
70 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080071
72KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070073
Clark Boylanb640e052014-04-03 16:41:46 -070074
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700106 categories = {'approved': ('Approved', -1, 1),
107 'code-review': ('Code-Review', -2, 2),
108 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700263 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
James E. Blairc2a5ed72017-02-20 14:12:01 -0500269 def getChangeMergedEvent(self):
270 event = {"submitter": {"name": "Jenkins",
271 "username": "jenkins"},
272 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
273 "patchSet": self.patchsets[-1],
274 "change": self.data,
275 "type": "change-merged",
276 "eventCreatedOn": 1487613810}
277 return event
278
James E. Blair8cce42e2016-10-18 08:18:36 -0700279 def getRefUpdatedEvent(self):
280 path = os.path.join(self.upstream_root, self.project)
281 repo = git.Repo(path)
282 oldrev = repo.heads[self.branch].commit.hexsha
283
284 event = {
285 "type": "ref-updated",
286 "submitter": {
287 "name": "User Name",
288 },
289 "refUpdate": {
290 "oldRev": oldrev,
291 "newRev": self.patchsets[-1]['revision'],
292 "refName": self.branch,
293 "project": self.project,
294 }
295 }
296 return event
297
Joshua Hesketh642824b2014-07-01 17:54:59 +1000298 def addApproval(self, category, value, username='reviewer_john',
299 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700300 if not granted_on:
301 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000302 approval = {
303 'description': self.categories[category][0],
304 'type': category,
305 'value': str(value),
306 'by': {
307 'username': username,
308 'email': username + '@example.com',
309 },
310 'grantedOn': int(granted_on)
311 }
Clark Boylanb640e052014-04-03 16:41:46 -0700312 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
313 if x['by']['username'] == username and x['type'] == category:
314 del self.patchsets[-1]['approvals'][i]
315 self.patchsets[-1]['approvals'].append(approval)
316 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000317 'author': {'email': 'author@example.com',
318 'name': 'Patchset Author',
319 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700320 'change': {'branch': self.branch,
321 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
322 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000323 'owner': {'email': 'owner@example.com',
324 'name': 'Change Owner',
325 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700326 'project': self.project,
327 'subject': self.subject,
328 'topic': 'master',
329 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700331 'patchSet': self.patchsets[-1],
332 'type': 'comment-added'}
333 self.data['submitRecords'] = self.getSubmitRecords()
334 return json.loads(json.dumps(event))
335
336 def getSubmitRecords(self):
337 status = {}
338 for cat in self.categories.keys():
339 status[cat] = 0
340
341 for a in self.patchsets[-1]['approvals']:
342 cur = status[a['type']]
343 cat_min, cat_max = self.categories[a['type']][1:]
344 new = int(a['value'])
345 if new == cat_min:
346 cur = new
347 elif abs(new) > abs(cur):
348 cur = new
349 status[a['type']] = cur
350
351 labels = []
352 ok = True
353 for typ, cat in self.categories.items():
354 cur = status[typ]
355 cat_min, cat_max = cat[1:]
356 if cur == cat_min:
357 value = 'REJECT'
358 ok = False
359 elif cur == cat_max:
360 value = 'OK'
361 else:
362 value = 'NEED'
363 ok = False
364 labels.append({'label': cat[0], 'status': value})
365 if ok:
366 return [{'status': 'OK'}]
367 return [{'status': 'NOT_READY',
368 'labels': labels}]
369
370 def setDependsOn(self, other, patchset):
371 self.depends_on_change = other
372 d = {'id': other.data['id'],
373 'number': other.data['number'],
374 'ref': other.patchsets[patchset - 1]['ref']
375 }
376 self.data['dependsOn'] = [d]
377
378 other.needed_by_changes.append(self)
379 needed = other.data.get('neededBy', [])
380 d = {'id': self.data['id'],
381 'number': self.data['number'],
382 'ref': self.patchsets[patchset - 1]['ref'],
383 'revision': self.patchsets[patchset - 1]['revision']
384 }
385 needed.append(d)
386 other.data['neededBy'] = needed
387
388 def query(self):
389 self.queried += 1
390 d = self.data.get('dependsOn')
391 if d:
392 d = d[0]
393 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
394 d['isCurrentPatchSet'] = True
395 else:
396 d['isCurrentPatchSet'] = False
397 return json.loads(json.dumps(self.data))
398
399 def setMerged(self):
400 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000401 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700402 return
403 if self.fail_merge:
404 return
405 self.data['status'] = 'MERGED'
406 self.open = False
407
408 path = os.path.join(self.upstream_root, self.project)
409 repo = git.Repo(path)
410 repo.heads[self.branch].commit = \
411 repo.commit(self.patchsets[-1]['revision'])
412
413 def setReported(self):
414 self.reported += 1
415
416
James E. Blaire511d2f2016-12-08 15:22:26 -0800417class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700418 """A Fake Gerrit connection for use in tests.
419
420 This subclasses
421 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
422 ability for tests to add changes to the fake Gerrit it represents.
423 """
424
Joshua Hesketh352264b2015-08-11 23:42:08 +1000425 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700426
James E. Blaire511d2f2016-12-08 15:22:26 -0800427 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700428 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800429 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000430 connection_config)
431
James E. Blair7fc8daa2016-08-08 15:37:15 -0700432 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700433 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
434 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000435 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700436 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200437 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700438
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700439 def addFakeChange(self, project, branch, subject, status='NEW',
440 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700441 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700442 self.change_number += 1
443 c = FakeChange(self, self.change_number, project, branch, subject,
444 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700445 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700446 self.changes[self.change_number] = c
447 return c
448
Clark Boylanb640e052014-04-03 16:41:46 -0700449 def review(self, project, changeid, message, action):
450 number, ps = changeid.split(',')
451 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000452
453 # Add the approval back onto the change (ie simulate what gerrit would
454 # do).
455 # Usually when zuul leaves a review it'll create a feedback loop where
456 # zuul's review enters another gerrit event (which is then picked up by
457 # zuul). However, we can't mimic this behaviour (by adding this
458 # approval event into the queue) as it stops jobs from checking what
459 # happens before this event is triggered. If a job needs to see what
460 # happens they can add their own verified event into the queue.
461 # Nevertheless, we can update change with the new review in gerrit.
462
James E. Blair8b5408c2016-08-08 15:37:46 -0700463 for cat in action.keys():
464 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000466
James E. Blair8b5408c2016-08-08 15:37:46 -0700467 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000468 if 'label' in action:
469 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000471
Clark Boylanb640e052014-04-03 16:41:46 -0700472 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000473
Clark Boylanb640e052014-04-03 16:41:46 -0700474 if 'submit' in action:
475 change.setMerged()
476 if message:
477 change.setReported()
478
479 def query(self, number):
480 change = self.changes.get(int(number))
481 if change:
482 return change.query()
483 return {}
484
James E. Blairc494d542014-08-06 09:23:52 -0700485 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700486 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700487 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800488 if query.startswith('change:'):
489 # Query a specific changeid
490 changeid = query[len('change:'):]
491 l = [change.query() for change in self.changes.values()
492 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700493 elif query.startswith('message:'):
494 # Query the content of a commit message
495 msg = query[len('message:'):].strip()
496 l = [change.query() for change in self.changes.values()
497 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800498 else:
499 # Query all open changes
500 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700501 return l
James E. Blairc494d542014-08-06 09:23:52 -0700502
Joshua Hesketh352264b2015-08-11 23:42:08 +1000503 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700504 pass
505
Joshua Hesketh352264b2015-08-11 23:42:08 +1000506 def getGitUrl(self, project):
507 return os.path.join(self.upstream_root, project.name)
508
Clark Boylanb640e052014-04-03 16:41:46 -0700509
510class BuildHistory(object):
511 def __init__(self, **kw):
512 self.__dict__.update(kw)
513
514 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700515 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
516 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700517
518
519class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200520 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700521 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700522 self.url = url
523
524 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700525 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700526 path = res.path
527 project = '/'.join(path.split('/')[2:-2])
528 ret = '001e# service=git-upload-pack\n'
529 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
530 'multi_ack thin-pack side-band side-band-64k ofs-delta '
531 'shallow no-progress include-tag multi_ack_detailed no-done\n')
532 path = os.path.join(self.upstream_root, project)
533 repo = git.Repo(path)
534 for ref in repo.refs:
535 r = ref.object.hexsha + ' ' + ref.path + '\n'
536 ret += '%04x%s' % (len(r) + 4, r)
537 ret += '0000'
538 return ret
539
540
Clark Boylanb640e052014-04-03 16:41:46 -0700541class FakeStatsd(threading.Thread):
542 def __init__(self):
543 threading.Thread.__init__(self)
544 self.daemon = True
545 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
546 self.sock.bind(('', 0))
547 self.port = self.sock.getsockname()[1]
548 self.wake_read, self.wake_write = os.pipe()
549 self.stats = []
550
551 def run(self):
552 while True:
553 poll = select.poll()
554 poll.register(self.sock, select.POLLIN)
555 poll.register(self.wake_read, select.POLLIN)
556 ret = poll.poll()
557 for (fd, event) in ret:
558 if fd == self.sock.fileno():
559 data = self.sock.recvfrom(1024)
560 if not data:
561 return
562 self.stats.append(data[0])
563 if fd == self.wake_read:
564 return
565
566 def stop(self):
567 os.write(self.wake_write, '1\n')
568
569
James E. Blaire1767bc2016-08-02 10:00:27 -0700570class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700571 log = logging.getLogger("zuul.test")
572
James E. Blair34776ee2016-08-25 13:53:54 -0700573 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700574 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700575 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700576 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700577 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700578 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700579 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700580 # TODOv3(jeblair): self.node is really "the image of the node
581 # assigned". We should rename it (self.node_image?) if we
582 # keep using it like this, or we may end up exposing more of
583 # the complexity around multi-node jobs here
584 # (self.nodes[0].image?)
585 self.node = None
586 if len(self.parameters.get('nodes')) == 1:
587 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700588 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100589 self.pipeline = self.parameters['ZUUL_PIPELINE']
590 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700591 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700592 self.wait_condition = threading.Condition()
593 self.waiting = False
594 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500595 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700596 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700597 self.changes = None
598 if 'ZUUL_CHANGE_IDS' in self.parameters:
599 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700600
James E. Blair3158e282016-08-19 09:34:11 -0700601 def __repr__(self):
602 waiting = ''
603 if self.waiting:
604 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100605 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
606 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700607
Clark Boylanb640e052014-04-03 16:41:46 -0700608 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700609 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700610 self.wait_condition.acquire()
611 self.wait_condition.notify()
612 self.waiting = False
613 self.log.debug("Build %s released" % self.unique)
614 self.wait_condition.release()
615
616 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700617 """Return whether this build is being held.
618
619 :returns: Whether the build is being held.
620 :rtype: bool
621 """
622
Clark Boylanb640e052014-04-03 16:41:46 -0700623 self.wait_condition.acquire()
624 if self.waiting:
625 ret = True
626 else:
627 ret = False
628 self.wait_condition.release()
629 return ret
630
631 def _wait(self):
632 self.wait_condition.acquire()
633 self.waiting = True
634 self.log.debug("Build %s waiting" % self.unique)
635 self.wait_condition.wait()
636 self.wait_condition.release()
637
638 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700639 self.log.debug('Running build %s' % self.unique)
640
James E. Blaire1767bc2016-08-02 10:00:27 -0700641 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700642 self.log.debug('Holding build %s' % self.unique)
643 self._wait()
644 self.log.debug("Build %s continuing" % self.unique)
645
James E. Blair412fba82017-01-26 15:00:50 -0800646 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700647 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800648 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700649 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800650 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500651 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800652 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700653
James E. Blaire1767bc2016-08-02 10:00:27 -0700654 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700655
James E. Blaira5dba232016-08-08 15:53:24 -0700656 def shouldFail(self):
657 changes = self.launch_server.fail_tests.get(self.name, [])
658 for change in changes:
659 if self.hasChanges(change):
660 return True
661 return False
662
James E. Blaire7b99a02016-08-05 14:27:34 -0700663 def hasChanges(self, *changes):
664 """Return whether this build has certain changes in its git repos.
665
666 :arg FakeChange changes: One or more changes (varargs) that
667 are expected to be present (in order) in the git repository of
668 the active project.
669
670 :returns: Whether the build has the indicated changes.
671 :rtype: bool
672
673 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800674 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500675 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800676 try:
677 repo = git.Repo(path)
678 except NoSuchPathError as e:
679 self.log.debug('%s' % e)
680 return False
681 ref = self.parameters['ZUUL_REF']
682 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
683 commit_message = '%s-1' % change.subject
684 self.log.debug("Checking if build %s has changes; commit_message "
685 "%s; repo_messages %s" % (self, commit_message,
686 repo_messages))
687 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700688 self.log.debug(" messages do not match")
689 return False
690 self.log.debug(" OK")
691 return True
692
Clark Boylanb640e052014-04-03 16:41:46 -0700693
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000694class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700695 """An Ansible launcher to be used in tests.
696
697 :ivar bool hold_jobs_in_build: If true, when jobs are launched
698 they will report that they have started but then pause until
699 released before reporting completion. This attribute may be
700 changed at any time and will take effect for subsequently
701 launched builds, but previously held builds will still need to
702 be explicitly released.
703
704 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800705 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700706 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800707 self._test_root = kw.pop('_test_root', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800708 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700709 self.hold_jobs_in_build = False
710 self.lock = threading.Lock()
711 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700712 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700713 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700714 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800715
James E. Blaira5dba232016-08-08 15:53:24 -0700716 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700717 """Instruct the launcher to report matching builds as failures.
718
719 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700720 :arg Change change: The :py:class:`~tests.base.FakeChange`
721 instance which should cause the job to fail. This job
722 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700723
724 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700725 l = self.fail_tests.get(name, [])
726 l.append(change)
727 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800728
James E. Blair962220f2016-08-03 11:22:38 -0700729 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700730 """Release a held build.
731
732 :arg str regex: A regular expression which, if supplied, will
733 cause only builds with matching names to be released. If
734 not supplied, all builds will be released.
735
736 """
James E. Blair962220f2016-08-03 11:22:38 -0700737 builds = self.running_builds[:]
738 self.log.debug("Releasing build %s (%s)" % (regex,
739 len(self.running_builds)))
740 for build in builds:
741 if not regex or re.match(regex, build.name):
742 self.log.debug("Releasing build %s" %
743 (build.parameters['ZUUL_UUID']))
744 build.release()
745 else:
746 self.log.debug("Not releasing build %s" %
747 (build.parameters['ZUUL_UUID']))
748 self.log.debug("Done releasing builds %s (%s)" %
749 (regex, len(self.running_builds)))
750
James E. Blair17302972016-08-10 16:11:42 -0700751 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700752 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700753 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700754 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700755 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800756 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500757 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800758 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100759 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
760 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700761
762 def stopJob(self, job):
763 self.log.debug("handle stop")
764 parameters = json.loads(job.arguments)
765 uuid = parameters['uuid']
766 for build in self.running_builds:
767 if build.unique == uuid:
768 build.aborted = True
769 build.release()
770 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700771
Joshua Hesketh50c21782016-10-13 21:34:14 +1100772
773class RecordingAnsibleJob(zuul.launcher.server.AnsibleJob):
Paul Belanger96618ed2017-03-01 09:42:33 -0500774 def runPlaybooks(self, args):
Joshua Hesketh50c21782016-10-13 21:34:14 +1100775 build = self.launcher_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800776 build.jobdir = self.jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700777
Paul Belanger96618ed2017-03-01 09:42:33 -0500778 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
James E. Blair412fba82017-01-26 15:00:50 -0800779
Joshua Hesketh50c21782016-10-13 21:34:14 +1100780 self.launcher_server.lock.acquire()
781 self.launcher_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700782 BuildHistory(name=build.name, result=result, changes=build.changes,
783 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800784 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700785 pipeline=build.parameters['ZUUL_PIPELINE'])
786 )
Joshua Hesketh50c21782016-10-13 21:34:14 +1100787 self.launcher_server.running_builds.remove(build)
788 del self.launcher_server.job_builds[self.job.unique]
789 self.launcher_server.lock.release()
James E. Blair412fba82017-01-26 15:00:50 -0800790 return result
791
Monty Taylore6562aa2017-02-20 07:37:39 -0500792 def runAnsible(self, cmd, timeout, trusted=False):
James E. Blair412fba82017-01-26 15:00:50 -0800793 build = self.launcher_server.job_builds[self.job.unique]
794
795 if self.launcher_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600796 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500797 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800798 else:
799 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700800 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800801
James E. Blairad8dca02017-02-21 11:48:32 -0500802 def getHostList(self, args):
803 self.log.debug("hostlist")
804 hosts = super(RecordingAnsibleJob, self).getHostList(args)
805 for name, d in hosts:
806 d['ansible_connection'] = 'local'
807 hosts.append(('localhost', dict(ansible_connection='local')))
808 return hosts
809
James E. Blairf5dbd002015-12-23 15:26:17 -0800810
Clark Boylanb640e052014-04-03 16:41:46 -0700811class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700812 """A Gearman server for use in tests.
813
814 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
815 added to the queue but will not be distributed to workers
816 until released. This attribute may be changed at any time and
817 will take effect for subsequently enqueued jobs, but
818 previously held jobs will still need to be explicitly
819 released.
820
821 """
822
Clark Boylanb640e052014-04-03 16:41:46 -0700823 def __init__(self):
824 self.hold_jobs_in_queue = False
825 super(FakeGearmanServer, self).__init__(0)
826
827 def getJobForConnection(self, connection, peek=False):
828 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
829 for job in queue:
830 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500831 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700832 job.waiting = self.hold_jobs_in_queue
833 else:
834 job.waiting = False
835 if job.waiting:
836 continue
837 if job.name in connection.functions:
838 if not peek:
839 queue.remove(job)
840 connection.related_jobs[job.handle] = job
841 job.worker_connection = connection
842 job.running = True
843 return job
844 return None
845
846 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700847 """Release a held job.
848
849 :arg str regex: A regular expression which, if supplied, will
850 cause only jobs with matching names to be released. If
851 not supplied, all jobs will be released.
852 """
Clark Boylanb640e052014-04-03 16:41:46 -0700853 released = False
854 qlen = (len(self.high_queue) + len(self.normal_queue) +
855 len(self.low_queue))
856 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
857 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500858 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700859 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500860 parameters = json.loads(job.arguments)
861 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700862 self.log.debug("releasing queued job %s" %
863 job.unique)
864 job.waiting = False
865 released = True
866 else:
867 self.log.debug("not releasing queued job %s" %
868 job.unique)
869 if released:
870 self.wakeConnections()
871 qlen = (len(self.high_queue) + len(self.normal_queue) +
872 len(self.low_queue))
873 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
874
875
876class FakeSMTP(object):
877 log = logging.getLogger('zuul.FakeSMTP')
878
879 def __init__(self, messages, server, port):
880 self.server = server
881 self.port = port
882 self.messages = messages
883
884 def sendmail(self, from_email, to_email, msg):
885 self.log.info("Sending email from %s, to %s, with msg %s" % (
886 from_email, to_email, msg))
887
888 headers = msg.split('\n\n', 1)[0]
889 body = msg.split('\n\n', 1)[1]
890
891 self.messages.append(dict(
892 from_email=from_email,
893 to_email=to_email,
894 msg=msg,
895 headers=headers,
896 body=body,
897 ))
898
899 return True
900
901 def quit(self):
902 return True
903
904
James E. Blairdce6cea2016-12-20 16:45:32 -0800905class FakeNodepool(object):
906 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800907 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800908
909 log = logging.getLogger("zuul.test.FakeNodepool")
910
911 def __init__(self, host, port, chroot):
912 self.client = kazoo.client.KazooClient(
913 hosts='%s:%s%s' % (host, port, chroot))
914 self.client.start()
915 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800916 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800917 self.thread = threading.Thread(target=self.run)
918 self.thread.daemon = True
919 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800920 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800921
922 def stop(self):
923 self._running = False
924 self.thread.join()
925 self.client.stop()
926 self.client.close()
927
928 def run(self):
929 while self._running:
930 self._run()
931 time.sleep(0.1)
932
933 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800934 if self.paused:
935 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800936 for req in self.getNodeRequests():
937 self.fulfillRequest(req)
938
939 def getNodeRequests(self):
940 try:
941 reqids = self.client.get_children(self.REQUEST_ROOT)
942 except kazoo.exceptions.NoNodeError:
943 return []
944 reqs = []
945 for oid in sorted(reqids):
946 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800947 try:
948 data, stat = self.client.get(path)
949 data = json.loads(data)
950 data['_oid'] = oid
951 reqs.append(data)
952 except kazoo.exceptions.NoNodeError:
953 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800954 return reqs
955
James E. Blaire18d4602017-01-05 11:17:28 -0800956 def getNodes(self):
957 try:
958 nodeids = self.client.get_children(self.NODE_ROOT)
959 except kazoo.exceptions.NoNodeError:
960 return []
961 nodes = []
962 for oid in sorted(nodeids):
963 path = self.NODE_ROOT + '/' + oid
964 data, stat = self.client.get(path)
965 data = json.loads(data)
966 data['_oid'] = oid
967 try:
968 lockfiles = self.client.get_children(path + '/lock')
969 except kazoo.exceptions.NoNodeError:
970 lockfiles = []
971 if lockfiles:
972 data['_lock'] = True
973 else:
974 data['_lock'] = False
975 nodes.append(data)
976 return nodes
977
James E. Blaira38c28e2017-01-04 10:33:20 -0800978 def makeNode(self, request_id, node_type):
979 now = time.time()
980 path = '/nodepool/nodes/'
981 data = dict(type=node_type,
982 provider='test-provider',
983 region='test-region',
984 az=None,
985 public_ipv4='127.0.0.1',
986 private_ipv4=None,
987 public_ipv6=None,
988 allocated_to=request_id,
989 state='ready',
990 state_time=now,
991 created_time=now,
992 updated_time=now,
993 image_id=None,
994 launcher='fake-nodepool')
995 data = json.dumps(data)
996 path = self.client.create(path, data,
997 makepath=True,
998 sequence=True)
999 nodeid = path.split("/")[-1]
1000 return nodeid
1001
James E. Blair6ab79e02017-01-06 10:10:17 -08001002 def addFailRequest(self, request):
1003 self.fail_requests.add(request['_oid'])
1004
James E. Blairdce6cea2016-12-20 16:45:32 -08001005 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001006 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001007 return
1008 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001009 oid = request['_oid']
1010 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001011
James E. Blair6ab79e02017-01-06 10:10:17 -08001012 if oid in self.fail_requests:
1013 request['state'] = 'failed'
1014 else:
1015 request['state'] = 'fulfilled'
1016 nodes = []
1017 for node in request['node_types']:
1018 nodeid = self.makeNode(oid, node)
1019 nodes.append(nodeid)
1020 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001021
James E. Blaira38c28e2017-01-04 10:33:20 -08001022 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001023 path = self.REQUEST_ROOT + '/' + oid
1024 data = json.dumps(request)
1025 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1026 self.client.set(path, data)
1027
1028
James E. Blair498059b2016-12-20 13:50:13 -08001029class ChrootedKazooFixture(fixtures.Fixture):
1030 def __init__(self):
1031 super(ChrootedKazooFixture, self).__init__()
1032
1033 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1034 if ':' in zk_host:
1035 host, port = zk_host.split(':')
1036 else:
1037 host = zk_host
1038 port = None
1039
1040 self.zookeeper_host = host
1041
1042 if not port:
1043 self.zookeeper_port = 2181
1044 else:
1045 self.zookeeper_port = int(port)
1046
1047 def _setUp(self):
1048 # Make sure the test chroot paths do not conflict
1049 random_bits = ''.join(random.choice(string.ascii_lowercase +
1050 string.ascii_uppercase)
1051 for x in range(8))
1052
1053 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1054 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1055
1056 # Ensure the chroot path exists and clean up any pre-existing znodes.
1057 _tmp_client = kazoo.client.KazooClient(
1058 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1059 _tmp_client.start()
1060
1061 if _tmp_client.exists(self.zookeeper_chroot):
1062 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1063
1064 _tmp_client.ensure_path(self.zookeeper_chroot)
1065 _tmp_client.stop()
1066 _tmp_client.close()
1067
1068 self.addCleanup(self._cleanup)
1069
1070 def _cleanup(self):
1071 '''Remove the chroot path.'''
1072 # Need a non-chroot'ed client to remove the chroot path
1073 _tmp_client = kazoo.client.KazooClient(
1074 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1075 _tmp_client.start()
1076 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1077 _tmp_client.stop()
1078
1079
Joshua Heskethd78b4482015-09-14 16:56:34 -06001080class MySQLSchemaFixture(fixtures.Fixture):
1081 def setUp(self):
1082 super(MySQLSchemaFixture, self).setUp()
1083
1084 random_bits = ''.join(random.choice(string.ascii_lowercase +
1085 string.ascii_uppercase)
1086 for x in range(8))
1087 self.name = '%s_%s' % (random_bits, os.getpid())
1088 self.passwd = uuid.uuid4().hex
1089 db = pymysql.connect(host="localhost",
1090 user="openstack_citest",
1091 passwd="openstack_citest",
1092 db="openstack_citest")
1093 cur = db.cursor()
1094 cur.execute("create database %s" % self.name)
1095 cur.execute(
1096 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1097 (self.name, self.name, self.passwd))
1098 cur.execute("flush privileges")
1099
1100 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1101 self.passwd,
1102 self.name)
1103 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1104 self.addCleanup(self.cleanup)
1105
1106 def cleanup(self):
1107 db = pymysql.connect(host="localhost",
1108 user="openstack_citest",
1109 passwd="openstack_citest",
1110 db="openstack_citest")
1111 cur = db.cursor()
1112 cur.execute("drop database %s" % self.name)
1113 cur.execute("drop user '%s'@'localhost'" % self.name)
1114 cur.execute("flush privileges")
1115
1116
Maru Newby3fe5f852015-01-13 04:22:14 +00001117class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001118 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001119 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001120
James E. Blair1c236df2017-02-01 14:07:24 -08001121 def attachLogs(self, *args):
1122 def reader():
1123 self._log_stream.seek(0)
1124 while True:
1125 x = self._log_stream.read(4096)
1126 if not x:
1127 break
1128 yield x.encode('utf8')
1129 content = testtools.content.content_from_reader(
1130 reader,
1131 testtools.content_type.UTF8_TEXT,
1132 False)
1133 self.addDetail('logging', content)
1134
Clark Boylanb640e052014-04-03 16:41:46 -07001135 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001136 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001137 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1138 try:
1139 test_timeout = int(test_timeout)
1140 except ValueError:
1141 # If timeout value is invalid do not set a timeout.
1142 test_timeout = 0
1143 if test_timeout > 0:
1144 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1145
1146 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1147 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1148 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1149 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1150 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1151 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1152 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1153 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1154 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1155 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001156 self._log_stream = StringIO()
1157 self.addOnException(self.attachLogs)
1158 else:
1159 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001160
James E. Blair1c236df2017-02-01 14:07:24 -08001161 handler = logging.StreamHandler(self._log_stream)
1162 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1163 '%(levelname)-8s %(message)s')
1164 handler.setFormatter(formatter)
1165
1166 logger = logging.getLogger()
1167 logger.setLevel(logging.DEBUG)
1168 logger.addHandler(handler)
1169
1170 # NOTE(notmorgan): Extract logging overrides for specific
1171 # libraries from the OS_LOG_DEFAULTS env and create loggers
1172 # for each. This is used to limit the output during test runs
1173 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001174 log_defaults_from_env = os.environ.get(
1175 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001176 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001177
James E. Blairdce6cea2016-12-20 16:45:32 -08001178 if log_defaults_from_env:
1179 for default in log_defaults_from_env.split(','):
1180 try:
1181 name, level_str = default.split('=', 1)
1182 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001183 logger = logging.getLogger(name)
1184 logger.setLevel(level)
1185 logger.addHandler(handler)
1186 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001187 except ValueError:
1188 # NOTE(notmorgan): Invalid format of the log default,
1189 # skip and don't try and apply a logger for the
1190 # specified module
1191 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001192
Maru Newby3fe5f852015-01-13 04:22:14 +00001193
1194class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001195 """A test case with a functioning Zuul.
1196
1197 The following class variables are used during test setup and can
1198 be overidden by subclasses but are effectively read-only once a
1199 test method starts running:
1200
1201 :cvar str config_file: This points to the main zuul config file
1202 within the fixtures directory. Subclasses may override this
1203 to obtain a different behavior.
1204
1205 :cvar str tenant_config_file: This is the tenant config file
1206 (which specifies from what git repos the configuration should
1207 be loaded). It defaults to the value specified in
1208 `config_file` but can be overidden by subclasses to obtain a
1209 different tenant/project layout while using the standard main
1210 configuration.
1211
1212 The following are instance variables that are useful within test
1213 methods:
1214
1215 :ivar FakeGerritConnection fake_<connection>:
1216 A :py:class:`~tests.base.FakeGerritConnection` will be
1217 instantiated for each connection present in the config file
1218 and stored here. For instance, `fake_gerrit` will hold the
1219 FakeGerritConnection object for a connection named `gerrit`.
1220
1221 :ivar FakeGearmanServer gearman_server: An instance of
1222 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1223 server that all of the Zuul components in this test use to
1224 communicate with each other.
1225
1226 :ivar RecordingLaunchServer launch_server: An instance of
1227 :py:class:`~tests.base.RecordingLaunchServer` which is the
1228 Ansible launch server used to run jobs for this test.
1229
1230 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1231 representing currently running builds. They are appended to
1232 the list in the order they are launched, and removed from this
1233 list upon completion.
1234
1235 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1236 objects representing completed builds. They are appended to
1237 the list in the order they complete.
1238
1239 """
1240
James E. Blair83005782015-12-11 14:46:03 -08001241 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001242 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001243
1244 def _startMerger(self):
1245 self.merge_server = zuul.merger.server.MergeServer(self.config,
1246 self.connections)
1247 self.merge_server.start()
1248
Maru Newby3fe5f852015-01-13 04:22:14 +00001249 def setUp(self):
1250 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001251
1252 self.setupZK()
1253
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001254 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001255 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001256 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1257 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001258 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001259 tmp_root = tempfile.mkdtemp(
1260 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001261 self.test_root = os.path.join(tmp_root, "zuul-test")
1262 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001263 self.merger_src_root = os.path.join(self.test_root, "merger-git")
1264 self.launcher_src_root = os.path.join(self.test_root, "launcher-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001265 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001266
1267 if os.path.exists(self.test_root):
1268 shutil.rmtree(self.test_root)
1269 os.makedirs(self.test_root)
1270 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001271 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001272
1273 # Make per test copy of Configuration.
1274 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001275 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001276 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001277 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001278 self.config.set('merger', 'git_dir', self.merger_src_root)
1279 self.config.set('launcher', 'git_dir', self.launcher_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001280 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001281
1282 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001283 # TODOv3(jeblair): remove these and replace with new git
1284 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001285 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001286 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001287 self.init_repo("org/project5")
1288 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001289 self.init_repo("org/one-job-project")
1290 self.init_repo("org/nonvoting-project")
1291 self.init_repo("org/templated-project")
1292 self.init_repo("org/layered-project")
1293 self.init_repo("org/node-project")
1294 self.init_repo("org/conflict-project")
1295 self.init_repo("org/noop-project")
1296 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001297 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001298
1299 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001300 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1301 # see: https://github.com/jsocol/pystatsd/issues/61
1302 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001303 os.environ['STATSD_PORT'] = str(self.statsd.port)
1304 self.statsd.start()
1305 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001306 reload_module(statsd)
1307 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001308
1309 self.gearman_server = FakeGearmanServer()
1310
1311 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001312 self.log.info("Gearman server on port %s" %
1313 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001314
James E. Blaire511d2f2016-12-08 15:22:26 -08001315 gerritsource.GerritSource.replication_timeout = 1.5
1316 gerritsource.GerritSource.replication_retry_interval = 0.5
1317 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001318
Joshua Hesketh352264b2015-08-11 23:42:08 +10001319 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001320
Jan Hruban6b71aff2015-10-22 16:58:08 +02001321 self.event_queues = [
1322 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001323 self.sched.trigger_event_queue,
1324 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001325 ]
1326
James E. Blairfef78942016-03-11 16:28:56 -08001327 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001328 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001329
Clark Boylanb640e052014-04-03 16:41:46 -07001330 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001331 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001332 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001333 return FakeURLOpener(self.upstream_root, *args, **kw)
1334
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001335 old_urlopen = urllib.request.urlopen
1336 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001337
James E. Blair3f876d52016-07-22 13:07:14 -07001338 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001339
James E. Blaire1767bc2016-08-02 10:00:27 -07001340 self.launch_server = RecordingLaunchServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001341 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001342 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001343 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001344 _test_root=self.test_root,
1345 keep_jobdir=KEEP_TEMPDIRS)
James E. Blaire1767bc2016-08-02 10:00:27 -07001346 self.launch_server.start()
1347 self.history = self.launch_server.build_history
1348 self.builds = self.launch_server.running_builds
1349
1350 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001351 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001352 self.merge_client = zuul.merger.client.MergeClient(
1353 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001354 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001355 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001356 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001357
James E. Blair0d5a36e2017-02-21 10:53:44 -05001358 self.fake_nodepool = FakeNodepool(
1359 self.zk_chroot_fixture.zookeeper_host,
1360 self.zk_chroot_fixture.zookeeper_port,
1361 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001362
James E. Blaire1767bc2016-08-02 10:00:27 -07001363 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001364 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001365 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001366 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001367
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001368 self.webapp = zuul.webapp.WebApp(
1369 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001370 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001371
1372 self.sched.start()
1373 self.sched.reconfigure(self.config)
1374 self.sched.resume()
1375 self.webapp.start()
1376 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001377 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001378
Clark Boylanb640e052014-04-03 16:41:46 -07001379 self.addCleanup(self.shutdown)
1380
James E. Blaire18d4602017-01-05 11:17:28 -08001381 def tearDown(self):
1382 super(ZuulTestCase, self).tearDown()
1383 self.assertFinalState()
1384
James E. Blairfef78942016-03-11 16:28:56 -08001385 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001386 # Set up gerrit related fakes
1387 # Set a changes database so multiple FakeGerrit's can report back to
1388 # a virtual canonical database given by the configured hostname
1389 self.gerrit_changes_dbs = {}
1390
1391 def getGerritConnection(driver, name, config):
1392 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1393 con = FakeGerritConnection(driver, name, config,
1394 changes_db=db,
1395 upstream_root=self.upstream_root)
1396 self.event_queues.append(con.event_queue)
1397 setattr(self, 'fake_' + name, con)
1398 return con
1399
1400 self.useFixture(fixtures.MonkeyPatch(
1401 'zuul.driver.gerrit.GerritDriver.getConnection',
1402 getGerritConnection))
1403
1404 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001405 # TODO(jhesketh): This should come from lib.connections for better
1406 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001407 # Register connections from the config
1408 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001409
Joshua Hesketh352264b2015-08-11 23:42:08 +10001410 def FakeSMTPFactory(*args, **kw):
1411 args = [self.smtp_messages] + list(args)
1412 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001413
Joshua Hesketh352264b2015-08-11 23:42:08 +10001414 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001415
James E. Blaire511d2f2016-12-08 15:22:26 -08001416 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001417 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001418 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001419
James E. Blair83005782015-12-11 14:46:03 -08001420 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001421 # This creates the per-test configuration object. It can be
1422 # overriden by subclasses, but should not need to be since it
1423 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001424 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001425 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001426 if hasattr(self, 'tenant_config_file'):
1427 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001428 git_path = os.path.join(
1429 os.path.dirname(
1430 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1431 'git')
1432 if os.path.exists(git_path):
1433 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001434 project = reponame.replace('_', '/')
1435 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001436 os.path.join(git_path, reponame))
1437
James E. Blair498059b2016-12-20 13:50:13 -08001438 def setupZK(self):
1439 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001440 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001441 self.zk_chroot_fixture.zookeeper_host,
1442 self.zk_chroot_fixture.zookeeper_port,
1443 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001444
James E. Blair96c6bf82016-01-15 16:20:40 -08001445 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001446 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001447
1448 files = {}
1449 for (dirpath, dirnames, filenames) in os.walk(source_path):
1450 for filename in filenames:
1451 test_tree_filepath = os.path.join(dirpath, filename)
1452 common_path = os.path.commonprefix([test_tree_filepath,
1453 source_path])
1454 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1455 with open(test_tree_filepath, 'r') as f:
1456 content = f.read()
1457 files[relative_filepath] = content
1458 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001459 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001460
James E. Blaire18d4602017-01-05 11:17:28 -08001461 def assertNodepoolState(self):
1462 # Make sure that there are no pending requests
1463
1464 requests = self.fake_nodepool.getNodeRequests()
1465 self.assertEqual(len(requests), 0)
1466
1467 nodes = self.fake_nodepool.getNodes()
1468 for node in nodes:
1469 self.assertFalse(node['_lock'], "Node %s is locked" %
1470 (node['_oid'],))
1471
Clark Boylanb640e052014-04-03 16:41:46 -07001472 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001473 # Make sure that git.Repo objects have been garbage collected.
1474 repos = []
1475 gc.collect()
1476 for obj in gc.get_objects():
1477 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001478 self.log.debug("Leaked git repo object: %s" % repr(obj))
1479 for r in gc.get_referrers(obj):
1480 self.log.debug(" referrer: %s" % repr(r))
Clark Boylanb640e052014-04-03 16:41:46 -07001481 repos.append(obj)
1482 self.assertEqual(len(repos), 0)
1483 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001484 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001485 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001486 for tenant in self.sched.abide.tenants.values():
1487 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001488 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001489 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001490
1491 def shutdown(self):
1492 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001493 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001494 self.merge_server.stop()
1495 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001496 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001497 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001498 self.sched.stop()
1499 self.sched.join()
1500 self.statsd.stop()
1501 self.statsd.join()
1502 self.webapp.stop()
1503 self.webapp.join()
1504 self.rpc.stop()
1505 self.rpc.join()
1506 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001507 self.fake_nodepool.stop()
1508 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001509 threads = threading.enumerate()
1510 if len(threads) > 1:
1511 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001512 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001513
1514 def init_repo(self, project):
1515 parts = project.split('/')
1516 path = os.path.join(self.upstream_root, *parts[:-1])
1517 if not os.path.exists(path):
1518 os.makedirs(path)
1519 path = os.path.join(self.upstream_root, project)
1520 repo = git.Repo.init(path)
1521
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001522 with repo.config_writer() as config_writer:
1523 config_writer.set_value('user', 'email', 'user@example.com')
1524 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001525
Clark Boylanb640e052014-04-03 16:41:46 -07001526 repo.index.commit('initial commit')
1527 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001528
James E. Blair97d902e2014-08-21 13:25:56 -07001529 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001530 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001531 repo.git.clean('-x', '-f', '-d')
1532
James E. Blair97d902e2014-08-21 13:25:56 -07001533 def create_branch(self, project, branch):
1534 path = os.path.join(self.upstream_root, project)
1535 repo = git.Repo.init(path)
1536 fn = os.path.join(path, 'README')
1537
1538 branch_head = repo.create_head(branch)
1539 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001540 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001541 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001542 f.close()
1543 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001544 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001545
James E. Blair97d902e2014-08-21 13:25:56 -07001546 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001547 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001548 repo.git.clean('-x', '-f', '-d')
1549
Sachi King9f16d522016-03-16 12:20:45 +11001550 def create_commit(self, project):
1551 path = os.path.join(self.upstream_root, project)
1552 repo = git.Repo(path)
1553 repo.head.reference = repo.heads['master']
1554 file_name = os.path.join(path, 'README')
1555 with open(file_name, 'a') as f:
1556 f.write('creating fake commit\n')
1557 repo.index.add([file_name])
1558 commit = repo.index.commit('Creating a fake commit')
1559 return commit.hexsha
1560
James E. Blairb8c16472015-05-05 14:55:26 -07001561 def orderedRelease(self):
1562 # Run one build at a time to ensure non-race order:
1563 while len(self.builds):
1564 self.release(self.builds[0])
1565 self.waitUntilSettled()
1566
Clark Boylanb640e052014-04-03 16:41:46 -07001567 def release(self, job):
1568 if isinstance(job, FakeBuild):
1569 job.release()
1570 else:
1571 job.waiting = False
1572 self.log.debug("Queued job %s released" % job.unique)
1573 self.gearman_server.wakeConnections()
1574
1575 def getParameter(self, job, name):
1576 if isinstance(job, FakeBuild):
1577 return job.parameters[name]
1578 else:
1579 parameters = json.loads(job.arguments)
1580 return parameters[name]
1581
Clark Boylanb640e052014-04-03 16:41:46 -07001582 def haveAllBuildsReported(self):
1583 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001584 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001585 return False
1586 # Find out if every build that the worker has completed has been
1587 # reported back to Zuul. If it hasn't then that means a Gearman
1588 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001589 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001590 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001591 if not zbuild:
1592 # It has already been reported
1593 continue
1594 # It hasn't been reported yet.
1595 return False
1596 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001597 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001598 if connection.state == 'GRAB_WAIT':
1599 return False
1600 return True
1601
1602 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001603 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001604 for build in builds:
1605 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001606 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001607 for j in conn.related_jobs.values():
1608 if j.unique == build.uuid:
1609 client_job = j
1610 break
1611 if not client_job:
1612 self.log.debug("%s is not known to the gearman client" %
1613 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001614 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001615 if not client_job.handle:
1616 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001617 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001618 server_job = self.gearman_server.jobs.get(client_job.handle)
1619 if not server_job:
1620 self.log.debug("%s is not known to the gearman server" %
1621 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001622 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001623 if not hasattr(server_job, 'waiting'):
1624 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001625 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001626 if server_job.waiting:
1627 continue
James E. Blair17302972016-08-10 16:11:42 -07001628 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001629 self.log.debug("%s has not reported start" % build)
1630 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001631 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001632 if worker_build:
1633 if worker_build.isWaiting():
1634 continue
1635 else:
1636 self.log.debug("%s is running" % worker_build)
1637 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001638 else:
James E. Blair962220f2016-08-03 11:22:38 -07001639 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001640 return False
1641 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001642
James E. Blairdce6cea2016-12-20 16:45:32 -08001643 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001644 if self.fake_nodepool.paused:
1645 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001646 if self.sched.nodepool.requests:
1647 return False
1648 return True
1649
Jan Hruban6b71aff2015-10-22 16:58:08 +02001650 def eventQueuesEmpty(self):
1651 for queue in self.event_queues:
1652 yield queue.empty()
1653
1654 def eventQueuesJoin(self):
1655 for queue in self.event_queues:
1656 queue.join()
1657
Clark Boylanb640e052014-04-03 16:41:46 -07001658 def waitUntilSettled(self):
1659 self.log.debug("Waiting until settled...")
1660 start = time.time()
1661 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001662 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001663 self.log.error("Timeout waiting for Zuul to settle")
1664 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001665 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001666 self.log.error(" %s: %s" % (queue, queue.empty()))
1667 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001668 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001669 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001670 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001671 self.log.error("All requests completed: %s" %
1672 (self.areAllNodeRequestsComplete(),))
1673 self.log.error("Merge client jobs: %s" %
1674 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001675 raise Exception("Timeout waiting for Zuul to settle")
1676 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001677
James E. Blaire1767bc2016-08-02 10:00:27 -07001678 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001679 # have all build states propogated to zuul?
1680 if self.haveAllBuildsReported():
1681 # Join ensures that the queue is empty _and_ events have been
1682 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001683 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001684 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001685 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001686 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001687 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001688 self.areAllNodeRequestsComplete() and
1689 all(self.eventQueuesEmpty())):
1690 # The queue empty check is placed at the end to
1691 # ensure that if a component adds an event between
1692 # when locked the run handler and checked that the
1693 # components were stable, we don't erroneously
1694 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001695 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001696 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001697 self.log.debug("...settled.")
1698 return
1699 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001700 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001701 self.sched.wake_event.wait(0.1)
1702
1703 def countJobResults(self, jobs, result):
1704 jobs = filter(lambda x: x.result == result, jobs)
1705 return len(jobs)
1706
James E. Blair96c6bf82016-01-15 16:20:40 -08001707 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001708 for job in self.history:
1709 if (job.name == name and
1710 (project is None or
1711 job.parameters['ZUUL_PROJECT'] == project)):
1712 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001713 raise Exception("Unable to find job %s in history" % name)
1714
1715 def assertEmptyQueues(self):
1716 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001717 for tenant in self.sched.abide.tenants.values():
1718 for pipeline in tenant.layout.pipelines.values():
1719 for queue in pipeline.queues:
1720 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001721 print('pipeline %s queue %s contents %s' % (
1722 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001723 self.assertEqual(len(queue.queue), 0,
1724 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001725
1726 def assertReportedStat(self, key, value=None, kind=None):
1727 start = time.time()
1728 while time.time() < (start + 5):
1729 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001730 k, v = stat.split(':')
1731 if key == k:
1732 if value is None and kind is None:
1733 return
1734 elif value:
1735 if value == v:
1736 return
1737 elif kind:
1738 if v.endswith('|' + kind):
1739 return
1740 time.sleep(0.1)
1741
Clark Boylanb640e052014-04-03 16:41:46 -07001742 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001743
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001744 def assertBuilds(self, builds):
1745 """Assert that the running builds are as described.
1746
1747 The list of running builds is examined and must match exactly
1748 the list of builds described by the input.
1749
1750 :arg list builds: A list of dictionaries. Each item in the
1751 list must match the corresponding build in the build
1752 history, and each element of the dictionary must match the
1753 corresponding attribute of the build.
1754
1755 """
James E. Blair3158e282016-08-19 09:34:11 -07001756 try:
1757 self.assertEqual(len(self.builds), len(builds))
1758 for i, d in enumerate(builds):
1759 for k, v in d.items():
1760 self.assertEqual(
1761 getattr(self.builds[i], k), v,
1762 "Element %i in builds does not match" % (i,))
1763 except Exception:
1764 for build in self.builds:
1765 self.log.error("Running build: %s" % build)
1766 else:
1767 self.log.error("No running builds")
1768 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001769
James E. Blairb536ecc2016-08-31 10:11:42 -07001770 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001771 """Assert that the completed builds are as described.
1772
1773 The list of completed builds is examined and must match
1774 exactly the list of builds described by the input.
1775
1776 :arg list history: A list of dictionaries. Each item in the
1777 list must match the corresponding build in the build
1778 history, and each element of the dictionary must match the
1779 corresponding attribute of the build.
1780
James E. Blairb536ecc2016-08-31 10:11:42 -07001781 :arg bool ordered: If true, the history must match the order
1782 supplied, if false, the builds are permitted to have
1783 arrived in any order.
1784
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001785 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001786 def matches(history_item, item):
1787 for k, v in item.items():
1788 if getattr(history_item, k) != v:
1789 return False
1790 return True
James E. Blair3158e282016-08-19 09:34:11 -07001791 try:
1792 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001793 if ordered:
1794 for i, d in enumerate(history):
1795 if not matches(self.history[i], d):
1796 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001797 "Element %i in history does not match %s" %
1798 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001799 else:
1800 unseen = self.history[:]
1801 for i, d in enumerate(history):
1802 found = False
1803 for unseen_item in unseen:
1804 if matches(unseen_item, d):
1805 found = True
1806 unseen.remove(unseen_item)
1807 break
1808 if not found:
1809 raise Exception("No match found for element %i "
1810 "in history" % (i,))
1811 if unseen:
1812 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001813 except Exception:
1814 for build in self.history:
1815 self.log.error("Completed build: %s" % build)
1816 else:
1817 self.log.error("No completed builds")
1818 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001819
James E. Blair6ac368c2016-12-22 18:07:20 -08001820 def printHistory(self):
1821 """Log the build history.
1822
1823 This can be useful during tests to summarize what jobs have
1824 completed.
1825
1826 """
1827 self.log.debug("Build history:")
1828 for build in self.history:
1829 self.log.debug(build)
1830
James E. Blair59fdbac2015-12-07 17:08:06 -08001831 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001832 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1833
1834 def updateConfigLayout(self, path):
1835 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001836 if not os.path.exists(root):
1837 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001838 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1839 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001840- tenant:
1841 name: openstack
1842 source:
1843 gerrit:
1844 config-repos:
1845 - %s
1846 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001847 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001848 self.config.set('zuul', 'tenant_config',
1849 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001850
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001851 def addCommitToRepo(self, project, message, files,
1852 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001853 path = os.path.join(self.upstream_root, project)
1854 repo = git.Repo(path)
1855 repo.head.reference = branch
1856 zuul.merger.merger.reset_repo_to_head(repo)
1857 for fn, content in files.items():
1858 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001859 try:
1860 os.makedirs(os.path.dirname(fn))
1861 except OSError:
1862 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001863 with open(fn, 'w') as f:
1864 f.write(content)
1865 repo.index.add([fn])
1866 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001867 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001868 repo.heads[branch].commit = commit
1869 repo.head.reference = branch
1870 repo.git.clean('-x', '-f', '-d')
1871 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001872 if tag:
1873 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001874 return before
1875
1876 def commitLayoutUpdate(self, orig_name, source_name):
1877 source_path = os.path.join(self.test_root, 'upstream',
1878 source_name, 'zuul.yaml')
1879 with open(source_path, 'r') as nt:
1880 before = self.addCommitToRepo(
1881 orig_name, 'Pulling content from %s' % source_name,
1882 {'zuul.yaml': nt.read()})
1883 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001884
James E. Blair7fc8daa2016-08-08 15:37:15 -07001885 def addEvent(self, connection, event):
1886 """Inject a Fake (Gerrit) event.
1887
1888 This method accepts a JSON-encoded event and simulates Zuul
1889 having received it from Gerrit. It could (and should)
1890 eventually apply to any connection type, but is currently only
1891 used with Gerrit connections. The name of the connection is
1892 used to look up the corresponding server, and the event is
1893 simulated as having been received by all Zuul connections
1894 attached to that server. So if two Gerrit connections in Zuul
1895 are connected to the same Gerrit server, and you invoke this
1896 method specifying the name of one of them, the event will be
1897 received by both.
1898
1899 .. note::
1900
1901 "self.fake_gerrit.addEvent" calls should be migrated to
1902 this method.
1903
1904 :arg str connection: The name of the connection corresponding
1905 to the gerrit server.
1906 :arg str event: The JSON-encoded event.
1907
1908 """
1909 specified_conn = self.connections.connections[connection]
1910 for conn in self.connections.connections.values():
1911 if (isinstance(conn, specified_conn.__class__) and
1912 specified_conn.server == conn.server):
1913 conn.addEvent(event)
1914
James E. Blair3f876d52016-07-22 13:07:14 -07001915
1916class AnsibleZuulTestCase(ZuulTestCase):
1917 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001918 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11001919
Joshua Heskethd78b4482015-09-14 16:56:34 -06001920
1921class ZuulDBTestCase(ZuulTestCase):
1922 def setup_config(self, config_file='zuul-connections-same-gerrit.conf'):
1923 super(ZuulDBTestCase, self).setup_config(config_file)
1924 for section_name in self.config.sections():
1925 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1926 section_name, re.I)
1927 if not con_match:
1928 continue
1929
1930 if self.config.get(section_name, 'driver') == 'sql':
1931 f = MySQLSchemaFixture()
1932 self.useFixture(f)
1933 if (self.config.get(section_name, 'dburi') ==
1934 '$MYSQL_FIXTURE_DBURI$'):
1935 self.config.set(section_name, 'dburi', f.dburi)