blob: d7bf467e06e78b525085672c8f175c549dcc46bd [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
Clark Boylan8208c192017-04-24 18:08:08 -070041import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070042import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import uuid
44
Clark Boylanb640e052014-04-03 16:41:46 -070045
46import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import statsd
53import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080054import testtools.content
55import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080056from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000057import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070058
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
63import zuul.webapp
64import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040065import zuul.executor.server
66import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080067import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070068import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070069import zuul.merger.merger
70import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
93 return hashlib.sha1(str(random.random())).hexdigest()
94
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700137 categories = {'approved': ('Approved', -1, 1),
138 'code-review': ('Code-Review', -2, 2),
139 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700140
141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
174 'url': 'https://hostname/%s' % number}
175
176 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
186 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
195 with open(fn, 'w') as f:
196 f.write(content)
197 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700198 else:
199 for fni in range(100):
200 fn = os.path.join(path, str(fni))
201 f = open(fn, 'w')
202 for ci in range(4096):
203 f.write(random.choice(string.printable))
204 f.close()
205 repo.index.add([fn])
206
207 r = repo.index.commit(msg)
208 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700209 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700210 repo.git.clean('-x', '-f', '-d')
211 repo.heads['master'].checkout()
212 return r
213
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700215 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700217 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 data = ("test %s %s %s\n" %
219 (self.branch, self.number, self.latest_patchset))
220 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700221 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700222 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700223 ps_files = [{'file': '/COMMIT_MSG',
224 'type': 'ADDED'},
225 {'file': 'README',
226 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700228 ps_files.append({'file': f, 'type': 'ADDED'})
229 d = {'approvals': [],
230 'createdOn': time.time(),
231 'files': ps_files,
232 'number': str(self.latest_patchset),
233 'ref': 'refs/changes/1/%s/%s' % (self.number,
234 self.latest_patchset),
235 'revision': c.hexsha,
236 'uploader': {'email': 'user@example.com',
237 'name': 'User name',
238 'username': 'user'}}
239 self.data['currentPatchSet'] = d
240 self.patchsets.append(d)
241 self.data['submitRecords'] = self.getSubmitRecords()
242
243 def getPatchsetCreatedEvent(self, patchset):
244 event = {"type": "patchset-created",
245 "change": {"project": self.project,
246 "branch": self.branch,
247 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
248 "number": str(self.number),
249 "subject": self.subject,
250 "owner": {"name": "User Name"},
251 "url": "https://hostname/3"},
252 "patchSet": self.patchsets[patchset - 1],
253 "uploader": {"name": "User Name"}}
254 return event
255
256 def getChangeRestoredEvent(self):
257 event = {"type": "change-restored",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100266 "patchSet": self.patchsets[-1],
267 "reason": ""}
268 return event
269
270 def getChangeAbandonedEvent(self):
271 event = {"type": "change-abandoned",
272 "change": {"project": self.project,
273 "branch": self.branch,
274 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
275 "number": str(self.number),
276 "subject": self.subject,
277 "owner": {"name": "User Name"},
278 "url": "https://hostname/3"},
279 "abandoner": {"name": "User Name"},
280 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700281 "reason": ""}
282 return event
283
284 def getChangeCommentEvent(self, patchset):
285 event = {"type": "comment-added",
286 "change": {"project": self.project,
287 "branch": self.branch,
288 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
289 "number": str(self.number),
290 "subject": self.subject,
291 "owner": {"name": "User Name"},
292 "url": "https://hostname/3"},
293 "patchSet": self.patchsets[patchset - 1],
294 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700295 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700296 "description": "Code-Review",
297 "value": "0"}],
298 "comment": "This is a comment"}
299 return event
300
James E. Blairc2a5ed72017-02-20 14:12:01 -0500301 def getChangeMergedEvent(self):
302 event = {"submitter": {"name": "Jenkins",
303 "username": "jenkins"},
304 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
305 "patchSet": self.patchsets[-1],
306 "change": self.data,
307 "type": "change-merged",
308 "eventCreatedOn": 1487613810}
309 return event
310
James E. Blair8cce42e2016-10-18 08:18:36 -0700311 def getRefUpdatedEvent(self):
312 path = os.path.join(self.upstream_root, self.project)
313 repo = git.Repo(path)
314 oldrev = repo.heads[self.branch].commit.hexsha
315
316 event = {
317 "type": "ref-updated",
318 "submitter": {
319 "name": "User Name",
320 },
321 "refUpdate": {
322 "oldRev": oldrev,
323 "newRev": self.patchsets[-1]['revision'],
324 "refName": self.branch,
325 "project": self.project,
326 }
327 }
328 return event
329
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 def addApproval(self, category, value, username='reviewer_john',
331 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700332 if not granted_on:
333 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000334 approval = {
335 'description': self.categories[category][0],
336 'type': category,
337 'value': str(value),
338 'by': {
339 'username': username,
340 'email': username + '@example.com',
341 },
342 'grantedOn': int(granted_on)
343 }
Clark Boylanb640e052014-04-03 16:41:46 -0700344 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
345 if x['by']['username'] == username and x['type'] == category:
346 del self.patchsets[-1]['approvals'][i]
347 self.patchsets[-1]['approvals'].append(approval)
348 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000349 'author': {'email': 'author@example.com',
350 'name': 'Patchset Author',
351 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700352 'change': {'branch': self.branch,
353 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
354 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'owner': {'email': 'owner@example.com',
356 'name': 'Change Owner',
357 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700358 'project': self.project,
359 'subject': self.subject,
360 'topic': 'master',
361 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'patchSet': self.patchsets[-1],
364 'type': 'comment-added'}
365 self.data['submitRecords'] = self.getSubmitRecords()
366 return json.loads(json.dumps(event))
367
368 def getSubmitRecords(self):
369 status = {}
370 for cat in self.categories.keys():
371 status[cat] = 0
372
373 for a in self.patchsets[-1]['approvals']:
374 cur = status[a['type']]
375 cat_min, cat_max = self.categories[a['type']][1:]
376 new = int(a['value'])
377 if new == cat_min:
378 cur = new
379 elif abs(new) > abs(cur):
380 cur = new
381 status[a['type']] = cur
382
383 labels = []
384 ok = True
385 for typ, cat in self.categories.items():
386 cur = status[typ]
387 cat_min, cat_max = cat[1:]
388 if cur == cat_min:
389 value = 'REJECT'
390 ok = False
391 elif cur == cat_max:
392 value = 'OK'
393 else:
394 value = 'NEED'
395 ok = False
396 labels.append({'label': cat[0], 'status': value})
397 if ok:
398 return [{'status': 'OK'}]
399 return [{'status': 'NOT_READY',
400 'labels': labels}]
401
402 def setDependsOn(self, other, patchset):
403 self.depends_on_change = other
404 d = {'id': other.data['id'],
405 'number': other.data['number'],
406 'ref': other.patchsets[patchset - 1]['ref']
407 }
408 self.data['dependsOn'] = [d]
409
410 other.needed_by_changes.append(self)
411 needed = other.data.get('neededBy', [])
412 d = {'id': self.data['id'],
413 'number': self.data['number'],
414 'ref': self.patchsets[patchset - 1]['ref'],
415 'revision': self.patchsets[patchset - 1]['revision']
416 }
417 needed.append(d)
418 other.data['neededBy'] = needed
419
420 def query(self):
421 self.queried += 1
422 d = self.data.get('dependsOn')
423 if d:
424 d = d[0]
425 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
426 d['isCurrentPatchSet'] = True
427 else:
428 d['isCurrentPatchSet'] = False
429 return json.loads(json.dumps(self.data))
430
431 def setMerged(self):
432 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000433 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700434 return
435 if self.fail_merge:
436 return
437 self.data['status'] = 'MERGED'
438 self.open = False
439
440 path = os.path.join(self.upstream_root, self.project)
441 repo = git.Repo(path)
442 repo.heads[self.branch].commit = \
443 repo.commit(self.patchsets[-1]['revision'])
444
445 def setReported(self):
446 self.reported += 1
447
448
James E. Blaire511d2f2016-12-08 15:22:26 -0800449class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700450 """A Fake Gerrit connection for use in tests.
451
452 This subclasses
453 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
454 ability for tests to add changes to the fake Gerrit it represents.
455 """
456
Joshua Hesketh352264b2015-08-11 23:42:08 +1000457 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000462 connection_config)
463
James E. Blair7fc8daa2016-08-08 15:37:15 -0700464 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700465 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
466 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700468 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200469 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700470
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700471 def addFakeChange(self, project, branch, subject, status='NEW',
472 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700473 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700475 c = FakeGerritChange(self, self.change_number, project, branch,
476 subject, upstream_root=self.upstream_root,
477 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700478 self.changes[self.change_number] = c
479 return c
480
Clark Boylanb640e052014-04-03 16:41:46 -0700481 def review(self, project, changeid, message, action):
482 number, ps = changeid.split(',')
483 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000484
485 # Add the approval back onto the change (ie simulate what gerrit would
486 # do).
487 # Usually when zuul leaves a review it'll create a feedback loop where
488 # zuul's review enters another gerrit event (which is then picked up by
489 # zuul). However, we can't mimic this behaviour (by adding this
490 # approval event into the queue) as it stops jobs from checking what
491 # happens before this event is triggered. If a job needs to see what
492 # happens they can add their own verified event into the queue.
493 # Nevertheless, we can update change with the new review in gerrit.
494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 for cat in action.keys():
496 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000497 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
James E. Blair8b5408c2016-08-08 15:37:46 -0700499 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500 if 'label' in action:
501 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000502 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000503
Clark Boylanb640e052014-04-03 16:41:46 -0700504 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000505
Clark Boylanb640e052014-04-03 16:41:46 -0700506 if 'submit' in action:
507 change.setMerged()
508 if message:
509 change.setReported()
510
511 def query(self, number):
512 change = self.changes.get(int(number))
513 if change:
514 return change.query()
515 return {}
516
James E. Blairc494d542014-08-06 09:23:52 -0700517 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700518 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700519 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800520 if query.startswith('change:'):
521 # Query a specific changeid
522 changeid = query[len('change:'):]
523 l = [change.query() for change in self.changes.values()
524 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700525 elif query.startswith('message:'):
526 # Query the content of a commit message
527 msg = query[len('message:'):].strip()
528 l = [change.query() for change in self.changes.values()
529 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800530 else:
531 # Query all open changes
532 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700533 return l
James E. Blairc494d542014-08-06 09:23:52 -0700534
Joshua Hesketh352264b2015-08-11 23:42:08 +1000535 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700536 pass
537
Joshua Hesketh352264b2015-08-11 23:42:08 +1000538 def getGitUrl(self, project):
539 return os.path.join(self.upstream_root, project.name)
540
Clark Boylanb640e052014-04-03 16:41:46 -0700541
Gregory Haynes4fc12542015-04-22 20:38:06 -0700542class GithubChangeReference(git.Reference):
543 _common_path_default = "refs/pull"
544 _points_to_commits_only = True
545
546
547class FakeGithubPullRequest(object):
548
549 def __init__(self, github, number, project, branch,
550 upstream_root, number_of_commits=1):
551 """Creates a new PR with several commits.
552 Sends an event about opened PR."""
553 self.github = github
554 self.source = github
555 self.number = number
556 self.project = project
557 self.branch = branch
558 self.upstream_root = upstream_root
559 self.comments = []
Jan Hrubane252a732017-01-03 15:03:09 +0100560 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700561 self.updated_at = None
562 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100563 self.is_merged = False
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self._createPRRef()
565 self._addCommitToRepo()
566 self._updateTimeStamp()
567
568 def addCommit(self):
569 """Adds a commit on top of the actual PR head."""
570 self._addCommitToRepo()
571 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100572 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700573
574 def forcePush(self):
575 """Clears actual commits and add a commit on top of the base."""
576 self._addCommitToRepo(reset=True)
577 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100578 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579
580 def getPullRequestOpenedEvent(self):
581 return self._getPullRequestEvent('opened')
582
583 def getPullRequestSynchronizeEvent(self):
584 return self._getPullRequestEvent('synchronize')
585
586 def getPullRequestReopenedEvent(self):
587 return self._getPullRequestEvent('reopened')
588
589 def getPullRequestClosedEvent(self):
590 return self._getPullRequestEvent('closed')
591
592 def addComment(self, message):
593 self.comments.append(message)
594 self._updateTimeStamp()
595
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200596 def getCommentAddedEvent(self, text):
597 name = 'issue_comment'
598 data = {
599 'action': 'created',
600 'issue': {
601 'number': self.number
602 },
603 'comment': {
604 'body': text
605 },
606 'repository': {
607 'full_name': self.project
608 }
609 }
610 return (name, data)
611
Gregory Haynes4fc12542015-04-22 20:38:06 -0700612 def _getRepo(self):
613 repo_path = os.path.join(self.upstream_root, self.project)
614 return git.Repo(repo_path)
615
616 def _createPRRef(self):
617 repo = self._getRepo()
618 GithubChangeReference.create(
619 repo, self._getPRReference(), 'refs/tags/init')
620
621 def _addCommitToRepo(self, reset=False):
622 repo = self._getRepo()
623 ref = repo.references[self._getPRReference()]
624 if reset:
625 ref.set_object('refs/tags/init')
626 repo.head.reference = ref
627 zuul.merger.merger.reset_repo_to_head(repo)
628 repo.git.clean('-x', '-f', '-d')
629
630 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
631 msg = 'test-%s' % self.number
632 fn = os.path.join(repo.working_dir, fn)
633 f = open(fn, 'w')
634 with open(fn, 'w') as f:
635 f.write("test %s %s\n" %
636 (self.branch, self.number))
637 repo.index.add([fn])
638
639 self.head_sha = repo.index.commit(msg).hexsha
640 repo.head.reference = 'master'
641 zuul.merger.merger.reset_repo_to_head(repo)
642 repo.git.clean('-x', '-f', '-d')
643 repo.heads['master'].checkout()
644
645 def _updateTimeStamp(self):
646 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
647
648 def getPRHeadSha(self):
649 repo = self._getRepo()
650 return repo.references[self._getPRReference()].commit.hexsha
651
Jan Hrubane252a732017-01-03 15:03:09 +0100652 def setStatus(self, state, url, description, context):
653 self.statuses[context] = {
654 'state': state,
655 'url': url,
656 'description': description
657 }
658
659 def _clearStatuses(self):
660 self.statuses = {}
661
Gregory Haynes4fc12542015-04-22 20:38:06 -0700662 def _getPRReference(self):
663 return '%s/head' % self.number
664
665 def _getPullRequestEvent(self, action):
666 name = 'pull_request'
667 data = {
668 'action': action,
669 'number': self.number,
670 'pull_request': {
671 'number': self.number,
672 'updated_at': self.updated_at,
673 'base': {
674 'ref': self.branch,
675 'repo': {
676 'full_name': self.project
677 }
678 },
679 'head': {
680 'sha': self.head_sha
681 }
682 }
683 }
684 return (name, data)
685
686
687class FakeGithubConnection(githubconnection.GithubConnection):
688 log = logging.getLogger("zuul.test.FakeGithubConnection")
689
690 def __init__(self, driver, connection_name, connection_config,
691 upstream_root=None):
692 super(FakeGithubConnection, self).__init__(driver, connection_name,
693 connection_config)
694 self.connection_name = connection_name
695 self.pr_number = 0
696 self.pull_requests = []
697 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100698 self.merge_failure = False
699 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700700
701 def openFakePullRequest(self, project, branch):
702 self.pr_number += 1
703 pull_request = FakeGithubPullRequest(
704 self, self.pr_number, project, branch, self.upstream_root)
705 self.pull_requests.append(pull_request)
706 return pull_request
707
Wayne1a78c612015-06-11 17:14:13 -0700708 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
709 if not old_rev:
710 old_rev = '00000000000000000000000000000000'
711 if not new_rev:
712 new_rev = random_sha1()
713 name = 'push'
714 data = {
715 'ref': ref,
716 'before': old_rev,
717 'after': new_rev,
718 'repository': {
719 'full_name': project
720 }
721 }
722 return (name, data)
723
Gregory Haynes4fc12542015-04-22 20:38:06 -0700724 def emitEvent(self, event):
725 """Emulates sending the GitHub webhook event to the connection."""
726 port = self.webapp.server.socket.getsockname()[1]
727 name, data = event
728 payload = json.dumps(data)
729 headers = {'X-Github-Event': name}
730 req = urllib.request.Request(
731 'http://localhost:%s/connection/%s/payload'
732 % (port, self.connection_name),
733 data=payload, headers=headers)
734 urllib.request.urlopen(req)
735
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200736 def getPull(self, project, number):
737 pr = self.pull_requests[number - 1]
738 data = {
739 'number': number,
740 'updated_at': pr.updated_at,
741 'base': {
742 'repo': {
743 'full_name': pr.project
744 },
745 'ref': pr.branch,
746 },
747 'head': {
748 'sha': pr.head_sha
749 }
750 }
751 return data
752
Gregory Haynes4fc12542015-04-22 20:38:06 -0700753 def getGitUrl(self, project):
754 return os.path.join(self.upstream_root, str(project))
755
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200756 def real_getGitUrl(self, project):
757 return super(FakeGithubConnection, self).getGitUrl(project)
758
Gregory Haynes4fc12542015-04-22 20:38:06 -0700759 def getProjectBranches(self, project):
760 """Masks getProjectBranches since we don't have a real github"""
761
762 # just returns master for now
763 return ['master']
764
Jan Hrubane252a732017-01-03 15:03:09 +0100765 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700766 pull_request = self.pull_requests[pr_number - 1]
767 pull_request.addComment(message)
768
Jan Hruban49bff072015-11-03 11:45:46 +0100769 def mergePull(self, project, pr_number, sha=None):
770 pull_request = self.pull_requests[pr_number - 1]
771 if self.merge_failure:
772 raise Exception('Pull request was not merged')
773 if self.merge_not_allowed_count > 0:
774 self.merge_not_allowed_count -= 1
775 raise MergeFailure('Merge was not successful due to mergeability'
776 ' conflict')
777 pull_request.is_merged = True
778
Jan Hrubane252a732017-01-03 15:03:09 +0100779 def setCommitStatus(self, project, sha, state,
780 url='', description='', context=''):
781 owner, proj = project.split('/')
782 for pr in self.pull_requests:
783 pr_owner, pr_project = pr.project.split('/')
784 if (pr_owner == owner and pr_project == proj and
785 pr.head_sha == sha):
786 pr.setStatus(state, url, description, context)
787
Gregory Haynes4fc12542015-04-22 20:38:06 -0700788
Clark Boylanb640e052014-04-03 16:41:46 -0700789class BuildHistory(object):
790 def __init__(self, **kw):
791 self.__dict__.update(kw)
792
793 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700794 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
795 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700796
797
798class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200799 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700800 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700801 self.url = url
802
803 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700804 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700805 path = res.path
806 project = '/'.join(path.split('/')[2:-2])
807 ret = '001e# service=git-upload-pack\n'
808 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
809 'multi_ack thin-pack side-band side-band-64k ofs-delta '
810 'shallow no-progress include-tag multi_ack_detailed no-done\n')
811 path = os.path.join(self.upstream_root, project)
812 repo = git.Repo(path)
813 for ref in repo.refs:
814 r = ref.object.hexsha + ' ' + ref.path + '\n'
815 ret += '%04x%s' % (len(r) + 4, r)
816 ret += '0000'
817 return ret
818
819
Clark Boylanb640e052014-04-03 16:41:46 -0700820class FakeStatsd(threading.Thread):
821 def __init__(self):
822 threading.Thread.__init__(self)
823 self.daemon = True
824 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
825 self.sock.bind(('', 0))
826 self.port = self.sock.getsockname()[1]
827 self.wake_read, self.wake_write = os.pipe()
828 self.stats = []
829
830 def run(self):
831 while True:
832 poll = select.poll()
833 poll.register(self.sock, select.POLLIN)
834 poll.register(self.wake_read, select.POLLIN)
835 ret = poll.poll()
836 for (fd, event) in ret:
837 if fd == self.sock.fileno():
838 data = self.sock.recvfrom(1024)
839 if not data:
840 return
841 self.stats.append(data[0])
842 if fd == self.wake_read:
843 return
844
845 def stop(self):
846 os.write(self.wake_write, '1\n')
847
848
James E. Blaire1767bc2016-08-02 10:00:27 -0700849class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700850 log = logging.getLogger("zuul.test")
851
Paul Belanger174a8272017-03-14 13:20:10 -0400852 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700853 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400854 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700855 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700856 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700857 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700858 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700859 # TODOv3(jeblair): self.node is really "the image of the node
860 # assigned". We should rename it (self.node_image?) if we
861 # keep using it like this, or we may end up exposing more of
862 # the complexity around multi-node jobs here
863 # (self.nodes[0].image?)
864 self.node = None
865 if len(self.parameters.get('nodes')) == 1:
866 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700867 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100868 self.pipeline = self.parameters['ZUUL_PIPELINE']
869 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700870 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700871 self.wait_condition = threading.Condition()
872 self.waiting = False
873 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500874 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700875 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700876 self.changes = None
877 if 'ZUUL_CHANGE_IDS' in self.parameters:
878 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700879
James E. Blair3158e282016-08-19 09:34:11 -0700880 def __repr__(self):
881 waiting = ''
882 if self.waiting:
883 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100884 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
885 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700886
Clark Boylanb640e052014-04-03 16:41:46 -0700887 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700888 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700889 self.wait_condition.acquire()
890 self.wait_condition.notify()
891 self.waiting = False
892 self.log.debug("Build %s released" % self.unique)
893 self.wait_condition.release()
894
895 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700896 """Return whether this build is being held.
897
898 :returns: Whether the build is being held.
899 :rtype: bool
900 """
901
Clark Boylanb640e052014-04-03 16:41:46 -0700902 self.wait_condition.acquire()
903 if self.waiting:
904 ret = True
905 else:
906 ret = False
907 self.wait_condition.release()
908 return ret
909
910 def _wait(self):
911 self.wait_condition.acquire()
912 self.waiting = True
913 self.log.debug("Build %s waiting" % self.unique)
914 self.wait_condition.wait()
915 self.wait_condition.release()
916
917 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700918 self.log.debug('Running build %s' % self.unique)
919
Paul Belanger174a8272017-03-14 13:20:10 -0400920 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700921 self.log.debug('Holding build %s' % self.unique)
922 self._wait()
923 self.log.debug("Build %s continuing" % self.unique)
924
James E. Blair412fba82017-01-26 15:00:50 -0800925 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700926 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800927 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700928 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800929 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500930 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800931 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700932
James E. Blaire1767bc2016-08-02 10:00:27 -0700933 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700934
James E. Blaira5dba232016-08-08 15:53:24 -0700935 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400936 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700937 for change in changes:
938 if self.hasChanges(change):
939 return True
940 return False
941
James E. Blaire7b99a02016-08-05 14:27:34 -0700942 def hasChanges(self, *changes):
943 """Return whether this build has certain changes in its git repos.
944
945 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700946 are expected to be present (in order) in the git repository of
947 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700948
949 :returns: Whether the build has the indicated changes.
950 :rtype: bool
951
952 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800953 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -0700954 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -0700955 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800956 try:
957 repo = git.Repo(path)
958 except NoSuchPathError as e:
959 self.log.debug('%s' % e)
960 return False
961 ref = self.parameters['ZUUL_REF']
962 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
963 commit_message = '%s-1' % change.subject
964 self.log.debug("Checking if build %s has changes; commit_message "
965 "%s; repo_messages %s" % (self, commit_message,
966 repo_messages))
967 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700968 self.log.debug(" messages do not match")
969 return False
970 self.log.debug(" OK")
971 return True
972
Clark Boylanb640e052014-04-03 16:41:46 -0700973
Paul Belanger174a8272017-03-14 13:20:10 -0400974class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
975 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700976
Paul Belanger174a8272017-03-14 13:20:10 -0400977 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700978 they will report that they have started but then pause until
979 released before reporting completion. This attribute may be
980 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400981 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700982 be explicitly released.
983
984 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800985 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700986 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800987 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400988 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700989 self.hold_jobs_in_build = False
990 self.lock = threading.Lock()
991 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700992 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700993 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700994 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800995
James E. Blaira5dba232016-08-08 15:53:24 -0700996 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400997 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700998
999 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001000 :arg Change change: The :py:class:`~tests.base.FakeChange`
1001 instance which should cause the job to fail. This job
1002 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001003
1004 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001005 l = self.fail_tests.get(name, [])
1006 l.append(change)
1007 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001008
James E. Blair962220f2016-08-03 11:22:38 -07001009 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001010 """Release a held build.
1011
1012 :arg str regex: A regular expression which, if supplied, will
1013 cause only builds with matching names to be released. If
1014 not supplied, all builds will be released.
1015
1016 """
James E. Blair962220f2016-08-03 11:22:38 -07001017 builds = self.running_builds[:]
1018 self.log.debug("Releasing build %s (%s)" % (regex,
1019 len(self.running_builds)))
1020 for build in builds:
1021 if not regex or re.match(regex, build.name):
1022 self.log.debug("Releasing build %s" %
1023 (build.parameters['ZUUL_UUID']))
1024 build.release()
1025 else:
1026 self.log.debug("Not releasing build %s" %
1027 (build.parameters['ZUUL_UUID']))
1028 self.log.debug("Done releasing builds %s (%s)" %
1029 (regex, len(self.running_builds)))
1030
Paul Belanger174a8272017-03-14 13:20:10 -04001031 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001032 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001033 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001034 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001035 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001036 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001037 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001038 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001039 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1040 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001041
1042 def stopJob(self, job):
1043 self.log.debug("handle stop")
1044 parameters = json.loads(job.arguments)
1045 uuid = parameters['uuid']
1046 for build in self.running_builds:
1047 if build.unique == uuid:
1048 build.aborted = True
1049 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001050 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001051
James E. Blaira002b032017-04-18 10:35:48 -07001052 def stop(self):
1053 for build in self.running_builds:
1054 build.release()
1055 super(RecordingExecutorServer, self).stop()
1056
Joshua Hesketh50c21782016-10-13 21:34:14 +11001057
Paul Belanger174a8272017-03-14 13:20:10 -04001058class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001059 def doMergeChanges(self, items):
1060 # Get a merger in order to update the repos involved in this job.
1061 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1062 if not commit: # merge conflict
1063 self.recordResult('MERGER_FAILURE')
1064 return commit
1065
1066 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001067 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001068 self.executor_server.lock.acquire()
1069 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001070 BuildHistory(name=build.name, result=result, changes=build.changes,
1071 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001072 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001073 pipeline=build.parameters['ZUUL_PIPELINE'])
1074 )
Paul Belanger174a8272017-03-14 13:20:10 -04001075 self.executor_server.running_builds.remove(build)
1076 del self.executor_server.job_builds[self.job.unique]
1077 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001078
1079 def runPlaybooks(self, args):
1080 build = self.executor_server.job_builds[self.job.unique]
1081 build.jobdir = self.jobdir
1082
1083 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1084 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001085 return result
1086
Monty Taylore6562aa2017-02-20 07:37:39 -05001087 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001088 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001089
Paul Belanger174a8272017-03-14 13:20:10 -04001090 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001091 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001092 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001093 else:
1094 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001095 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001096
James E. Blairad8dca02017-02-21 11:48:32 -05001097 def getHostList(self, args):
1098 self.log.debug("hostlist")
1099 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001100 for host in hosts:
1101 host['host_vars']['ansible_connection'] = 'local'
1102
1103 hosts.append(dict(
1104 name='localhost',
1105 host_vars=dict(ansible_connection='local'),
1106 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001107 return hosts
1108
James E. Blairf5dbd002015-12-23 15:26:17 -08001109
Clark Boylanb640e052014-04-03 16:41:46 -07001110class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001111 """A Gearman server for use in tests.
1112
1113 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1114 added to the queue but will not be distributed to workers
1115 until released. This attribute may be changed at any time and
1116 will take effect for subsequently enqueued jobs, but
1117 previously held jobs will still need to be explicitly
1118 released.
1119
1120 """
1121
Clark Boylanb640e052014-04-03 16:41:46 -07001122 def __init__(self):
1123 self.hold_jobs_in_queue = False
1124 super(FakeGearmanServer, self).__init__(0)
1125
1126 def getJobForConnection(self, connection, peek=False):
1127 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1128 for job in queue:
1129 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001130 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001131 job.waiting = self.hold_jobs_in_queue
1132 else:
1133 job.waiting = False
1134 if job.waiting:
1135 continue
1136 if job.name in connection.functions:
1137 if not peek:
1138 queue.remove(job)
1139 connection.related_jobs[job.handle] = job
1140 job.worker_connection = connection
1141 job.running = True
1142 return job
1143 return None
1144
1145 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001146 """Release a held job.
1147
1148 :arg str regex: A regular expression which, if supplied, will
1149 cause only jobs with matching names to be released. If
1150 not supplied, all jobs will be released.
1151 """
Clark Boylanb640e052014-04-03 16:41:46 -07001152 released = False
1153 qlen = (len(self.high_queue) + len(self.normal_queue) +
1154 len(self.low_queue))
1155 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1156 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001157 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001158 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001159 parameters = json.loads(job.arguments)
1160 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001161 self.log.debug("releasing queued job %s" %
1162 job.unique)
1163 job.waiting = False
1164 released = True
1165 else:
1166 self.log.debug("not releasing queued job %s" %
1167 job.unique)
1168 if released:
1169 self.wakeConnections()
1170 qlen = (len(self.high_queue) + len(self.normal_queue) +
1171 len(self.low_queue))
1172 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1173
1174
1175class FakeSMTP(object):
1176 log = logging.getLogger('zuul.FakeSMTP')
1177
1178 def __init__(self, messages, server, port):
1179 self.server = server
1180 self.port = port
1181 self.messages = messages
1182
1183 def sendmail(self, from_email, to_email, msg):
1184 self.log.info("Sending email from %s, to %s, with msg %s" % (
1185 from_email, to_email, msg))
1186
1187 headers = msg.split('\n\n', 1)[0]
1188 body = msg.split('\n\n', 1)[1]
1189
1190 self.messages.append(dict(
1191 from_email=from_email,
1192 to_email=to_email,
1193 msg=msg,
1194 headers=headers,
1195 body=body,
1196 ))
1197
1198 return True
1199
1200 def quit(self):
1201 return True
1202
1203
James E. Blairdce6cea2016-12-20 16:45:32 -08001204class FakeNodepool(object):
1205 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001206 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001207
1208 log = logging.getLogger("zuul.test.FakeNodepool")
1209
1210 def __init__(self, host, port, chroot):
1211 self.client = kazoo.client.KazooClient(
1212 hosts='%s:%s%s' % (host, port, chroot))
1213 self.client.start()
1214 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001215 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001216 self.thread = threading.Thread(target=self.run)
1217 self.thread.daemon = True
1218 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001219 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001220
1221 def stop(self):
1222 self._running = False
1223 self.thread.join()
1224 self.client.stop()
1225 self.client.close()
1226
1227 def run(self):
1228 while self._running:
1229 self._run()
1230 time.sleep(0.1)
1231
1232 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001233 if self.paused:
1234 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001235 for req in self.getNodeRequests():
1236 self.fulfillRequest(req)
1237
1238 def getNodeRequests(self):
1239 try:
1240 reqids = self.client.get_children(self.REQUEST_ROOT)
1241 except kazoo.exceptions.NoNodeError:
1242 return []
1243 reqs = []
1244 for oid in sorted(reqids):
1245 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001246 try:
1247 data, stat = self.client.get(path)
1248 data = json.loads(data)
1249 data['_oid'] = oid
1250 reqs.append(data)
1251 except kazoo.exceptions.NoNodeError:
1252 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001253 return reqs
1254
James E. Blaire18d4602017-01-05 11:17:28 -08001255 def getNodes(self):
1256 try:
1257 nodeids = self.client.get_children(self.NODE_ROOT)
1258 except kazoo.exceptions.NoNodeError:
1259 return []
1260 nodes = []
1261 for oid in sorted(nodeids):
1262 path = self.NODE_ROOT + '/' + oid
1263 data, stat = self.client.get(path)
1264 data = json.loads(data)
1265 data['_oid'] = oid
1266 try:
1267 lockfiles = self.client.get_children(path + '/lock')
1268 except kazoo.exceptions.NoNodeError:
1269 lockfiles = []
1270 if lockfiles:
1271 data['_lock'] = True
1272 else:
1273 data['_lock'] = False
1274 nodes.append(data)
1275 return nodes
1276
James E. Blaira38c28e2017-01-04 10:33:20 -08001277 def makeNode(self, request_id, node_type):
1278 now = time.time()
1279 path = '/nodepool/nodes/'
1280 data = dict(type=node_type,
1281 provider='test-provider',
1282 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001283 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001284 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001285 public_ipv4='127.0.0.1',
1286 private_ipv4=None,
1287 public_ipv6=None,
1288 allocated_to=request_id,
1289 state='ready',
1290 state_time=now,
1291 created_time=now,
1292 updated_time=now,
1293 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001294 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001295 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001296 data = json.dumps(data)
1297 path = self.client.create(path, data,
1298 makepath=True,
1299 sequence=True)
1300 nodeid = path.split("/")[-1]
1301 return nodeid
1302
James E. Blair6ab79e02017-01-06 10:10:17 -08001303 def addFailRequest(self, request):
1304 self.fail_requests.add(request['_oid'])
1305
James E. Blairdce6cea2016-12-20 16:45:32 -08001306 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001307 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001308 return
1309 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001310 oid = request['_oid']
1311 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001312
James E. Blair6ab79e02017-01-06 10:10:17 -08001313 if oid in self.fail_requests:
1314 request['state'] = 'failed'
1315 else:
1316 request['state'] = 'fulfilled'
1317 nodes = []
1318 for node in request['node_types']:
1319 nodeid = self.makeNode(oid, node)
1320 nodes.append(nodeid)
1321 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001322
James E. Blaira38c28e2017-01-04 10:33:20 -08001323 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001324 path = self.REQUEST_ROOT + '/' + oid
1325 data = json.dumps(request)
1326 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1327 self.client.set(path, data)
1328
1329
James E. Blair498059b2016-12-20 13:50:13 -08001330class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001331 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001332 super(ChrootedKazooFixture, self).__init__()
1333
1334 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1335 if ':' in zk_host:
1336 host, port = zk_host.split(':')
1337 else:
1338 host = zk_host
1339 port = None
1340
1341 self.zookeeper_host = host
1342
1343 if not port:
1344 self.zookeeper_port = 2181
1345 else:
1346 self.zookeeper_port = int(port)
1347
Clark Boylan621ec9a2017-04-07 17:41:33 -07001348 self.test_id = test_id
1349
James E. Blair498059b2016-12-20 13:50:13 -08001350 def _setUp(self):
1351 # Make sure the test chroot paths do not conflict
1352 random_bits = ''.join(random.choice(string.ascii_lowercase +
1353 string.ascii_uppercase)
1354 for x in range(8))
1355
Clark Boylan621ec9a2017-04-07 17:41:33 -07001356 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001357 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1358
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001359 self.addCleanup(self._cleanup)
1360
James E. Blair498059b2016-12-20 13:50:13 -08001361 # Ensure the chroot path exists and clean up any pre-existing znodes.
1362 _tmp_client = kazoo.client.KazooClient(
1363 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1364 _tmp_client.start()
1365
1366 if _tmp_client.exists(self.zookeeper_chroot):
1367 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1368
1369 _tmp_client.ensure_path(self.zookeeper_chroot)
1370 _tmp_client.stop()
1371 _tmp_client.close()
1372
James E. Blair498059b2016-12-20 13:50:13 -08001373 def _cleanup(self):
1374 '''Remove the chroot path.'''
1375 # Need a non-chroot'ed client to remove the chroot path
1376 _tmp_client = kazoo.client.KazooClient(
1377 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1378 _tmp_client.start()
1379 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1380 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001381 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001382
1383
Joshua Heskethd78b4482015-09-14 16:56:34 -06001384class MySQLSchemaFixture(fixtures.Fixture):
1385 def setUp(self):
1386 super(MySQLSchemaFixture, self).setUp()
1387
1388 random_bits = ''.join(random.choice(string.ascii_lowercase +
1389 string.ascii_uppercase)
1390 for x in range(8))
1391 self.name = '%s_%s' % (random_bits, os.getpid())
1392 self.passwd = uuid.uuid4().hex
1393 db = pymysql.connect(host="localhost",
1394 user="openstack_citest",
1395 passwd="openstack_citest",
1396 db="openstack_citest")
1397 cur = db.cursor()
1398 cur.execute("create database %s" % self.name)
1399 cur.execute(
1400 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1401 (self.name, self.name, self.passwd))
1402 cur.execute("flush privileges")
1403
1404 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1405 self.passwd,
1406 self.name)
1407 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1408 self.addCleanup(self.cleanup)
1409
1410 def cleanup(self):
1411 db = pymysql.connect(host="localhost",
1412 user="openstack_citest",
1413 passwd="openstack_citest",
1414 db="openstack_citest")
1415 cur = db.cursor()
1416 cur.execute("drop database %s" % self.name)
1417 cur.execute("drop user '%s'@'localhost'" % self.name)
1418 cur.execute("flush privileges")
1419
1420
Maru Newby3fe5f852015-01-13 04:22:14 +00001421class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001422 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001423 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001424
James E. Blair1c236df2017-02-01 14:07:24 -08001425 def attachLogs(self, *args):
1426 def reader():
1427 self._log_stream.seek(0)
1428 while True:
1429 x = self._log_stream.read(4096)
1430 if not x:
1431 break
1432 yield x.encode('utf8')
1433 content = testtools.content.content_from_reader(
1434 reader,
1435 testtools.content_type.UTF8_TEXT,
1436 False)
1437 self.addDetail('logging', content)
1438
Clark Boylanb640e052014-04-03 16:41:46 -07001439 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001440 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001441 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1442 try:
1443 test_timeout = int(test_timeout)
1444 except ValueError:
1445 # If timeout value is invalid do not set a timeout.
1446 test_timeout = 0
1447 if test_timeout > 0:
1448 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1449
1450 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1451 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1452 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1453 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1454 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1455 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1456 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1457 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1458 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1459 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001460 self._log_stream = StringIO()
1461 self.addOnException(self.attachLogs)
1462 else:
1463 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001464
James E. Blair1c236df2017-02-01 14:07:24 -08001465 handler = logging.StreamHandler(self._log_stream)
1466 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1467 '%(levelname)-8s %(message)s')
1468 handler.setFormatter(formatter)
1469
1470 logger = logging.getLogger()
1471 logger.setLevel(logging.DEBUG)
1472 logger.addHandler(handler)
1473
Clark Boylan3410d532017-04-25 12:35:29 -07001474 # Make sure we don't carry old handlers around in process state
1475 # which slows down test runs
1476 self.addCleanup(logger.removeHandler, handler)
1477 self.addCleanup(handler.close)
1478 self.addCleanup(handler.flush)
1479
James E. Blair1c236df2017-02-01 14:07:24 -08001480 # NOTE(notmorgan): Extract logging overrides for specific
1481 # libraries from the OS_LOG_DEFAULTS env and create loggers
1482 # for each. This is used to limit the output during test runs
1483 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001484 log_defaults_from_env = os.environ.get(
1485 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001486 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001487
James E. Blairdce6cea2016-12-20 16:45:32 -08001488 if log_defaults_from_env:
1489 for default in log_defaults_from_env.split(','):
1490 try:
1491 name, level_str = default.split('=', 1)
1492 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001493 logger = logging.getLogger(name)
1494 logger.setLevel(level)
1495 logger.addHandler(handler)
1496 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001497 except ValueError:
1498 # NOTE(notmorgan): Invalid format of the log default,
1499 # skip and don't try and apply a logger for the
1500 # specified module
1501 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001502
Maru Newby3fe5f852015-01-13 04:22:14 +00001503
1504class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001505 """A test case with a functioning Zuul.
1506
1507 The following class variables are used during test setup and can
1508 be overidden by subclasses but are effectively read-only once a
1509 test method starts running:
1510
1511 :cvar str config_file: This points to the main zuul config file
1512 within the fixtures directory. Subclasses may override this
1513 to obtain a different behavior.
1514
1515 :cvar str tenant_config_file: This is the tenant config file
1516 (which specifies from what git repos the configuration should
1517 be loaded). It defaults to the value specified in
1518 `config_file` but can be overidden by subclasses to obtain a
1519 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001520 configuration. See also the :py:func:`simple_layout`
1521 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001522
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001523 :cvar bool create_project_keys: Indicates whether Zuul should
1524 auto-generate keys for each project, or whether the test
1525 infrastructure should insert dummy keys to save time during
1526 startup. Defaults to False.
1527
James E. Blaire7b99a02016-08-05 14:27:34 -07001528 The following are instance variables that are useful within test
1529 methods:
1530
1531 :ivar FakeGerritConnection fake_<connection>:
1532 A :py:class:`~tests.base.FakeGerritConnection` will be
1533 instantiated for each connection present in the config file
1534 and stored here. For instance, `fake_gerrit` will hold the
1535 FakeGerritConnection object for a connection named `gerrit`.
1536
1537 :ivar FakeGearmanServer gearman_server: An instance of
1538 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1539 server that all of the Zuul components in this test use to
1540 communicate with each other.
1541
Paul Belanger174a8272017-03-14 13:20:10 -04001542 :ivar RecordingExecutorServer executor_server: An instance of
1543 :py:class:`~tests.base.RecordingExecutorServer` which is the
1544 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001545
1546 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1547 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001548 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001549 list upon completion.
1550
1551 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1552 objects representing completed builds. They are appended to
1553 the list in the order they complete.
1554
1555 """
1556
James E. Blair83005782015-12-11 14:46:03 -08001557 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001558 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001559 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001560
1561 def _startMerger(self):
1562 self.merge_server = zuul.merger.server.MergeServer(self.config,
1563 self.connections)
1564 self.merge_server.start()
1565
Maru Newby3fe5f852015-01-13 04:22:14 +00001566 def setUp(self):
1567 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001568
1569 self.setupZK()
1570
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001571 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001572 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001573 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1574 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001575 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001576 tmp_root = tempfile.mkdtemp(
1577 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001578 self.test_root = os.path.join(tmp_root, "zuul-test")
1579 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001580 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001581 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001582 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001583
1584 if os.path.exists(self.test_root):
1585 shutil.rmtree(self.test_root)
1586 os.makedirs(self.test_root)
1587 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001588 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001589
1590 # Make per test copy of Configuration.
1591 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001592 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001593 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001594 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001595 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001596 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001597 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001598
Clark Boylanb640e052014-04-03 16:41:46 -07001599 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001600 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1601 # see: https://github.com/jsocol/pystatsd/issues/61
1602 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001603 os.environ['STATSD_PORT'] = str(self.statsd.port)
1604 self.statsd.start()
1605 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001606 reload_module(statsd)
1607 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001608
1609 self.gearman_server = FakeGearmanServer()
1610
1611 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001612 self.log.info("Gearman server on port %s" %
1613 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001614
James E. Blaire511d2f2016-12-08 15:22:26 -08001615 gerritsource.GerritSource.replication_timeout = 1.5
1616 gerritsource.GerritSource.replication_retry_interval = 0.5
1617 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001618
Joshua Hesketh352264b2015-08-11 23:42:08 +10001619 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001620
Jan Hruban7083edd2015-08-21 14:00:54 +02001621 self.webapp = zuul.webapp.WebApp(
1622 self.sched, port=0, listen_address='127.0.0.1')
1623
Jan Hruban6b71aff2015-10-22 16:58:08 +02001624 self.event_queues = [
1625 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001626 self.sched.trigger_event_queue,
1627 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001628 ]
1629
James E. Blairfef78942016-03-11 16:28:56 -08001630 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001631 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001632
Clark Boylanb640e052014-04-03 16:41:46 -07001633 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001634 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001635 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001636 return FakeURLOpener(self.upstream_root, *args, **kw)
1637
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001638 old_urlopen = urllib.request.urlopen
1639 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001640
James E. Blair3f876d52016-07-22 13:07:14 -07001641 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001642
Paul Belanger174a8272017-03-14 13:20:10 -04001643 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001644 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001645 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001646 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001647 _test_root=self.test_root,
1648 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001649 self.executor_server.start()
1650 self.history = self.executor_server.build_history
1651 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001652
Paul Belanger174a8272017-03-14 13:20:10 -04001653 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001654 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001655 self.merge_client = zuul.merger.client.MergeClient(
1656 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001657 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001658 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001659 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001660
James E. Blair0d5a36e2017-02-21 10:53:44 -05001661 self.fake_nodepool = FakeNodepool(
1662 self.zk_chroot_fixture.zookeeper_host,
1663 self.zk_chroot_fixture.zookeeper_port,
1664 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001665
Paul Belanger174a8272017-03-14 13:20:10 -04001666 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001667 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001668 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001669 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001670
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001671 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001672
1673 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001674 self.webapp.start()
1675 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001676 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001677 # Cleanups are run in reverse order
1678 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001679 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001680 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001681
James E. Blairb9c0d772017-03-03 14:34:49 -08001682 self.sched.reconfigure(self.config)
1683 self.sched.resume()
1684
James E. Blairfef78942016-03-11 16:28:56 -08001685 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001686 # Set up gerrit related fakes
1687 # Set a changes database so multiple FakeGerrit's can report back to
1688 # a virtual canonical database given by the configured hostname
1689 self.gerrit_changes_dbs = {}
1690
1691 def getGerritConnection(driver, name, config):
1692 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1693 con = FakeGerritConnection(driver, name, config,
1694 changes_db=db,
1695 upstream_root=self.upstream_root)
1696 self.event_queues.append(con.event_queue)
1697 setattr(self, 'fake_' + name, con)
1698 return con
1699
1700 self.useFixture(fixtures.MonkeyPatch(
1701 'zuul.driver.gerrit.GerritDriver.getConnection',
1702 getGerritConnection))
1703
Gregory Haynes4fc12542015-04-22 20:38:06 -07001704 def getGithubConnection(driver, name, config):
1705 con = FakeGithubConnection(driver, name, config,
1706 upstream_root=self.upstream_root)
1707 setattr(self, 'fake_' + name, con)
1708 return con
1709
1710 self.useFixture(fixtures.MonkeyPatch(
1711 'zuul.driver.github.GithubDriver.getConnection',
1712 getGithubConnection))
1713
James E. Blaire511d2f2016-12-08 15:22:26 -08001714 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001715 # TODO(jhesketh): This should come from lib.connections for better
1716 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001717 # Register connections from the config
1718 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001719
Joshua Hesketh352264b2015-08-11 23:42:08 +10001720 def FakeSMTPFactory(*args, **kw):
1721 args = [self.smtp_messages] + list(args)
1722 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001723
Joshua Hesketh352264b2015-08-11 23:42:08 +10001724 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001725
James E. Blaire511d2f2016-12-08 15:22:26 -08001726 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001727 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001728 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001729
James E. Blair83005782015-12-11 14:46:03 -08001730 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001731 # This creates the per-test configuration object. It can be
1732 # overriden by subclasses, but should not need to be since it
1733 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001734 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001735 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001736
1737 if not self.setupSimpleLayout():
1738 if hasattr(self, 'tenant_config_file'):
1739 self.config.set('zuul', 'tenant_config',
1740 self.tenant_config_file)
1741 git_path = os.path.join(
1742 os.path.dirname(
1743 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1744 'git')
1745 if os.path.exists(git_path):
1746 for reponame in os.listdir(git_path):
1747 project = reponame.replace('_', '/')
1748 self.copyDirToRepo(project,
1749 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001750 self.setupAllProjectKeys()
1751
James E. Blair06cc3922017-04-19 10:08:10 -07001752 def setupSimpleLayout(self):
1753 # If the test method has been decorated with a simple_layout,
1754 # use that instead of the class tenant_config_file. Set up a
1755 # single config-project with the specified layout, and
1756 # initialize repos for all of the 'project' entries which
1757 # appear in the layout.
1758 test_name = self.id().split('.')[-1]
1759 test = getattr(self, test_name)
1760 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001761 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001762 else:
1763 return False
1764
James E. Blairb70e55a2017-04-19 12:57:02 -07001765 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001766 path = os.path.join(FIXTURE_DIR, path)
1767 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001768 data = f.read()
1769 layout = yaml.safe_load(data)
1770 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001771 untrusted_projects = []
1772 for item in layout:
1773 if 'project' in item:
1774 name = item['project']['name']
1775 untrusted_projects.append(name)
1776 self.init_repo(name)
1777 self.addCommitToRepo(name, 'initial commit',
1778 files={'README': ''},
1779 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001780 if 'job' in item:
1781 jobname = item['job']['name']
1782 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001783
1784 root = os.path.join(self.test_root, "config")
1785 if not os.path.exists(root):
1786 os.makedirs(root)
1787 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1788 config = [{'tenant':
1789 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001790 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001791 {'config-projects': ['common-config'],
1792 'untrusted-projects': untrusted_projects}}}}]
1793 f.write(yaml.dump(config))
1794 f.close()
1795 self.config.set('zuul', 'tenant_config',
1796 os.path.join(FIXTURE_DIR, f.name))
1797
1798 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001799 self.addCommitToRepo('common-config', 'add content from fixture',
1800 files, branch='master', tag='init')
1801
1802 return True
1803
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001804 def setupAllProjectKeys(self):
1805 if self.create_project_keys:
1806 return
1807
1808 path = self.config.get('zuul', 'tenant_config')
1809 with open(os.path.join(FIXTURE_DIR, path)) as f:
1810 tenant_config = yaml.safe_load(f.read())
1811 for tenant in tenant_config:
1812 sources = tenant['tenant']['source']
1813 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001814 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001815 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001816 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001817 self.setupProjectKeys(source, project)
1818
1819 def setupProjectKeys(self, source, project):
1820 # Make sure we set up an RSA key for the project so that we
1821 # don't spend time generating one:
1822
1823 key_root = os.path.join(self.state_root, 'keys')
1824 if not os.path.isdir(key_root):
1825 os.mkdir(key_root, 0o700)
1826 private_key_file = os.path.join(key_root, source, project + '.pem')
1827 private_key_dir = os.path.dirname(private_key_file)
1828 self.log.debug("Installing test keys for project %s at %s" % (
1829 project, private_key_file))
1830 if not os.path.isdir(private_key_dir):
1831 os.makedirs(private_key_dir)
1832 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1833 with open(private_key_file, 'w') as o:
1834 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001835
James E. Blair498059b2016-12-20 13:50:13 -08001836 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001837 self.zk_chroot_fixture = self.useFixture(
1838 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001839 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001840 self.zk_chroot_fixture.zookeeper_host,
1841 self.zk_chroot_fixture.zookeeper_port,
1842 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001843
James E. Blair96c6bf82016-01-15 16:20:40 -08001844 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001845 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001846
1847 files = {}
1848 for (dirpath, dirnames, filenames) in os.walk(source_path):
1849 for filename in filenames:
1850 test_tree_filepath = os.path.join(dirpath, filename)
1851 common_path = os.path.commonprefix([test_tree_filepath,
1852 source_path])
1853 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1854 with open(test_tree_filepath, 'r') as f:
1855 content = f.read()
1856 files[relative_filepath] = content
1857 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001858 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001859
James E. Blaire18d4602017-01-05 11:17:28 -08001860 def assertNodepoolState(self):
1861 # Make sure that there are no pending requests
1862
1863 requests = self.fake_nodepool.getNodeRequests()
1864 self.assertEqual(len(requests), 0)
1865
1866 nodes = self.fake_nodepool.getNodes()
1867 for node in nodes:
1868 self.assertFalse(node['_lock'], "Node %s is locked" %
1869 (node['_oid'],))
1870
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001871 def assertNoGeneratedKeys(self):
1872 # Make sure that Zuul did not generate any project keys
1873 # (unless it was supposed to).
1874
1875 if self.create_project_keys:
1876 return
1877
1878 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1879 test_key = i.read()
1880
1881 key_root = os.path.join(self.state_root, 'keys')
1882 for root, dirname, files in os.walk(key_root):
1883 for fn in files:
1884 with open(os.path.join(root, fn)) as f:
1885 self.assertEqual(test_key, f.read())
1886
Clark Boylanb640e052014-04-03 16:41:46 -07001887 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001888 self.log.debug("Assert final state")
1889 # Make sure no jobs are running
1890 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001891 # Make sure that git.Repo objects have been garbage collected.
1892 repos = []
1893 gc.collect()
1894 for obj in gc.get_objects():
1895 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001896 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001897 repos.append(obj)
1898 self.assertEqual(len(repos), 0)
1899 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001900 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001901 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001902 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001903 for tenant in self.sched.abide.tenants.values():
1904 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001905 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001906 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001907
1908 def shutdown(self):
1909 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001910 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001911 self.merge_server.stop()
1912 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001913 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001914 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001915 self.sched.stop()
1916 self.sched.join()
1917 self.statsd.stop()
1918 self.statsd.join()
1919 self.webapp.stop()
1920 self.webapp.join()
1921 self.rpc.stop()
1922 self.rpc.join()
1923 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001924 self.fake_nodepool.stop()
1925 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001926 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001927 # we whitelist watchdog threads as they have relatively long delays
1928 # before noticing they should exit, but they should exit on their own.
1929 threads = [t for t in threading.enumerate()
1930 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001931 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001932 log_str = ""
1933 for thread_id, stack_frame in sys._current_frames().items():
1934 log_str += "Thread: %s\n" % thread_id
1935 log_str += "".join(traceback.format_stack(stack_frame))
1936 self.log.debug(log_str)
1937 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001938
James E. Blaira002b032017-04-18 10:35:48 -07001939 def assertCleanShutdown(self):
1940 pass
1941
James E. Blairc4ba97a2017-04-19 16:26:24 -07001942 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001943 parts = project.split('/')
1944 path = os.path.join(self.upstream_root, *parts[:-1])
1945 if not os.path.exists(path):
1946 os.makedirs(path)
1947 path = os.path.join(self.upstream_root, project)
1948 repo = git.Repo.init(path)
1949
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001950 with repo.config_writer() as config_writer:
1951 config_writer.set_value('user', 'email', 'user@example.com')
1952 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001953
Clark Boylanb640e052014-04-03 16:41:46 -07001954 repo.index.commit('initial commit')
1955 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001956 if tag:
1957 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001958
James E. Blair97d902e2014-08-21 13:25:56 -07001959 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001960 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001961 repo.git.clean('-x', '-f', '-d')
1962
James E. Blair97d902e2014-08-21 13:25:56 -07001963 def create_branch(self, project, branch):
1964 path = os.path.join(self.upstream_root, project)
1965 repo = git.Repo.init(path)
1966 fn = os.path.join(path, 'README')
1967
1968 branch_head = repo.create_head(branch)
1969 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001970 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001971 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001972 f.close()
1973 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001974 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001975
James E. Blair97d902e2014-08-21 13:25:56 -07001976 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001977 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001978 repo.git.clean('-x', '-f', '-d')
1979
Sachi King9f16d522016-03-16 12:20:45 +11001980 def create_commit(self, project):
1981 path = os.path.join(self.upstream_root, project)
1982 repo = git.Repo(path)
1983 repo.head.reference = repo.heads['master']
1984 file_name = os.path.join(path, 'README')
1985 with open(file_name, 'a') as f:
1986 f.write('creating fake commit\n')
1987 repo.index.add([file_name])
1988 commit = repo.index.commit('Creating a fake commit')
1989 return commit.hexsha
1990
James E. Blairf4a5f022017-04-18 14:01:10 -07001991 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001992 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001993 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001994 while len(self.builds):
1995 self.release(self.builds[0])
1996 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001997 i += 1
1998 if count is not None and i >= count:
1999 break
James E. Blairb8c16472015-05-05 14:55:26 -07002000
Clark Boylanb640e052014-04-03 16:41:46 -07002001 def release(self, job):
2002 if isinstance(job, FakeBuild):
2003 job.release()
2004 else:
2005 job.waiting = False
2006 self.log.debug("Queued job %s released" % job.unique)
2007 self.gearman_server.wakeConnections()
2008
2009 def getParameter(self, job, name):
2010 if isinstance(job, FakeBuild):
2011 return job.parameters[name]
2012 else:
2013 parameters = json.loads(job.arguments)
2014 return parameters[name]
2015
Clark Boylanb640e052014-04-03 16:41:46 -07002016 def haveAllBuildsReported(self):
2017 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002018 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002019 return False
2020 # Find out if every build that the worker has completed has been
2021 # reported back to Zuul. If it hasn't then that means a Gearman
2022 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002023 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002024 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002025 if not zbuild:
2026 # It has already been reported
2027 continue
2028 # It hasn't been reported yet.
2029 return False
2030 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002031 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002032 if connection.state == 'GRAB_WAIT':
2033 return False
2034 return True
2035
2036 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002037 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002038 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002039 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002040 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002041 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002042 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002043 for j in conn.related_jobs.values():
2044 if j.unique == build.uuid:
2045 client_job = j
2046 break
2047 if not client_job:
2048 self.log.debug("%s is not known to the gearman client" %
2049 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002050 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002051 if not client_job.handle:
2052 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002053 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002054 server_job = self.gearman_server.jobs.get(client_job.handle)
2055 if not server_job:
2056 self.log.debug("%s is not known to the gearman server" %
2057 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002058 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002059 if not hasattr(server_job, 'waiting'):
2060 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002061 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002062 if server_job.waiting:
2063 continue
James E. Blair17302972016-08-10 16:11:42 -07002064 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002065 self.log.debug("%s has not reported start" % build)
2066 return False
Paul Belanger174a8272017-03-14 13:20:10 -04002067 worker_build = self.executor_server.job_builds.get(
2068 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07002069 if worker_build:
2070 if worker_build.isWaiting():
2071 continue
2072 else:
2073 self.log.debug("%s is running" % worker_build)
2074 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002075 else:
James E. Blair962220f2016-08-03 11:22:38 -07002076 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002077 return False
James E. Blaira002b032017-04-18 10:35:48 -07002078 for (build_uuid, job_worker) in \
2079 self.executor_server.job_workers.items():
2080 if build_uuid not in seen_builds:
2081 self.log.debug("%s is not finalized" % build_uuid)
2082 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002083 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002084
James E. Blairdce6cea2016-12-20 16:45:32 -08002085 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002086 if self.fake_nodepool.paused:
2087 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002088 if self.sched.nodepool.requests:
2089 return False
2090 return True
2091
Jan Hruban6b71aff2015-10-22 16:58:08 +02002092 def eventQueuesEmpty(self):
2093 for queue in self.event_queues:
2094 yield queue.empty()
2095
2096 def eventQueuesJoin(self):
2097 for queue in self.event_queues:
2098 queue.join()
2099
Clark Boylanb640e052014-04-03 16:41:46 -07002100 def waitUntilSettled(self):
2101 self.log.debug("Waiting until settled...")
2102 start = time.time()
2103 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002104 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002105 self.log.error("Timeout waiting for Zuul to settle")
2106 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002107 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002108 self.log.error(" %s: %s" % (queue, queue.empty()))
2109 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002110 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002111 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002112 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002113 self.log.error("All requests completed: %s" %
2114 (self.areAllNodeRequestsComplete(),))
2115 self.log.error("Merge client jobs: %s" %
2116 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002117 raise Exception("Timeout waiting for Zuul to settle")
2118 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002119
Paul Belanger174a8272017-03-14 13:20:10 -04002120 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002121 # have all build states propogated to zuul?
2122 if self.haveAllBuildsReported():
2123 # Join ensures that the queue is empty _and_ events have been
2124 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002125 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002126 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002127 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002128 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002129 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002130 self.areAllNodeRequestsComplete() and
2131 all(self.eventQueuesEmpty())):
2132 # The queue empty check is placed at the end to
2133 # ensure that if a component adds an event between
2134 # when locked the run handler and checked that the
2135 # components were stable, we don't erroneously
2136 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002137 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002138 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002139 self.log.debug("...settled.")
2140 return
2141 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002142 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002143 self.sched.wake_event.wait(0.1)
2144
2145 def countJobResults(self, jobs, result):
2146 jobs = filter(lambda x: x.result == result, jobs)
2147 return len(jobs)
2148
James E. Blair96c6bf82016-01-15 16:20:40 -08002149 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002150 for job in self.history:
2151 if (job.name == name and
2152 (project is None or
2153 job.parameters['ZUUL_PROJECT'] == project)):
2154 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002155 raise Exception("Unable to find job %s in history" % name)
2156
2157 def assertEmptyQueues(self):
2158 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002159 for tenant in self.sched.abide.tenants.values():
2160 for pipeline in tenant.layout.pipelines.values():
2161 for queue in pipeline.queues:
2162 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002163 print('pipeline %s queue %s contents %s' % (
2164 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002165 self.assertEqual(len(queue.queue), 0,
2166 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002167
2168 def assertReportedStat(self, key, value=None, kind=None):
2169 start = time.time()
2170 while time.time() < (start + 5):
2171 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002172 k, v = stat.split(':')
2173 if key == k:
2174 if value is None and kind is None:
2175 return
2176 elif value:
2177 if value == v:
2178 return
2179 elif kind:
2180 if v.endswith('|' + kind):
2181 return
2182 time.sleep(0.1)
2183
Clark Boylanb640e052014-04-03 16:41:46 -07002184 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002185
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002186 def assertBuilds(self, builds):
2187 """Assert that the running builds are as described.
2188
2189 The list of running builds is examined and must match exactly
2190 the list of builds described by the input.
2191
2192 :arg list builds: A list of dictionaries. Each item in the
2193 list must match the corresponding build in the build
2194 history, and each element of the dictionary must match the
2195 corresponding attribute of the build.
2196
2197 """
James E. Blair3158e282016-08-19 09:34:11 -07002198 try:
2199 self.assertEqual(len(self.builds), len(builds))
2200 for i, d in enumerate(builds):
2201 for k, v in d.items():
2202 self.assertEqual(
2203 getattr(self.builds[i], k), v,
2204 "Element %i in builds does not match" % (i,))
2205 except Exception:
2206 for build in self.builds:
2207 self.log.error("Running build: %s" % build)
2208 else:
2209 self.log.error("No running builds")
2210 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002211
James E. Blairb536ecc2016-08-31 10:11:42 -07002212 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002213 """Assert that the completed builds are as described.
2214
2215 The list of completed builds is examined and must match
2216 exactly the list of builds described by the input.
2217
2218 :arg list history: A list of dictionaries. Each item in the
2219 list must match the corresponding build in the build
2220 history, and each element of the dictionary must match the
2221 corresponding attribute of the build.
2222
James E. Blairb536ecc2016-08-31 10:11:42 -07002223 :arg bool ordered: If true, the history must match the order
2224 supplied, if false, the builds are permitted to have
2225 arrived in any order.
2226
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002227 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002228 def matches(history_item, item):
2229 for k, v in item.items():
2230 if getattr(history_item, k) != v:
2231 return False
2232 return True
James E. Blair3158e282016-08-19 09:34:11 -07002233 try:
2234 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002235 if ordered:
2236 for i, d in enumerate(history):
2237 if not matches(self.history[i], d):
2238 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002239 "Element %i in history does not match %s" %
2240 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002241 else:
2242 unseen = self.history[:]
2243 for i, d in enumerate(history):
2244 found = False
2245 for unseen_item in unseen:
2246 if matches(unseen_item, d):
2247 found = True
2248 unseen.remove(unseen_item)
2249 break
2250 if not found:
2251 raise Exception("No match found for element %i "
2252 "in history" % (i,))
2253 if unseen:
2254 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002255 except Exception:
2256 for build in self.history:
2257 self.log.error("Completed build: %s" % build)
2258 else:
2259 self.log.error("No completed builds")
2260 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002261
James E. Blair6ac368c2016-12-22 18:07:20 -08002262 def printHistory(self):
2263 """Log the build history.
2264
2265 This can be useful during tests to summarize what jobs have
2266 completed.
2267
2268 """
2269 self.log.debug("Build history:")
2270 for build in self.history:
2271 self.log.debug(build)
2272
James E. Blair59fdbac2015-12-07 17:08:06 -08002273 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002274 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2275
James E. Blair9ea70072017-04-19 16:05:30 -07002276 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002277 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002278 if not os.path.exists(root):
2279 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002280 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2281 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002282- tenant:
2283 name: openstack
2284 source:
2285 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002286 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002287 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002288 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002289 - org/project
2290 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002291 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002292 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002293 self.config.set('zuul', 'tenant_config',
2294 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002295 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002296
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002297 def addCommitToRepo(self, project, message, files,
2298 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002299 path = os.path.join(self.upstream_root, project)
2300 repo = git.Repo(path)
2301 repo.head.reference = branch
2302 zuul.merger.merger.reset_repo_to_head(repo)
2303 for fn, content in files.items():
2304 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002305 try:
2306 os.makedirs(os.path.dirname(fn))
2307 except OSError:
2308 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002309 with open(fn, 'w') as f:
2310 f.write(content)
2311 repo.index.add([fn])
2312 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002313 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002314 repo.heads[branch].commit = commit
2315 repo.head.reference = branch
2316 repo.git.clean('-x', '-f', '-d')
2317 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002318 if tag:
2319 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002320 return before
2321
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002322 def commitConfigUpdate(self, project_name, source_name):
2323 """Commit an update to zuul.yaml
2324
2325 This overwrites the zuul.yaml in the specificed project with
2326 the contents specified.
2327
2328 :arg str project_name: The name of the project containing
2329 zuul.yaml (e.g., common-config)
2330
2331 :arg str source_name: The path to the file (underneath the
2332 test fixture directory) whose contents should be used to
2333 replace zuul.yaml.
2334 """
2335
2336 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002337 files = {}
2338 with open(source_path, 'r') as f:
2339 data = f.read()
2340 layout = yaml.safe_load(data)
2341 files['zuul.yaml'] = data
2342 for item in layout:
2343 if 'job' in item:
2344 jobname = item['job']['name']
2345 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002346 before = self.addCommitToRepo(
2347 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002348 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002349 return before
2350
James E. Blair7fc8daa2016-08-08 15:37:15 -07002351 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002352
James E. Blair7fc8daa2016-08-08 15:37:15 -07002353 """Inject a Fake (Gerrit) event.
2354
2355 This method accepts a JSON-encoded event and simulates Zuul
2356 having received it from Gerrit. It could (and should)
2357 eventually apply to any connection type, but is currently only
2358 used with Gerrit connections. The name of the connection is
2359 used to look up the corresponding server, and the event is
2360 simulated as having been received by all Zuul connections
2361 attached to that server. So if two Gerrit connections in Zuul
2362 are connected to the same Gerrit server, and you invoke this
2363 method specifying the name of one of them, the event will be
2364 received by both.
2365
2366 .. note::
2367
2368 "self.fake_gerrit.addEvent" calls should be migrated to
2369 this method.
2370
2371 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002372 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002373 :arg str event: The JSON-encoded event.
2374
2375 """
2376 specified_conn = self.connections.connections[connection]
2377 for conn in self.connections.connections.values():
2378 if (isinstance(conn, specified_conn.__class__) and
2379 specified_conn.server == conn.server):
2380 conn.addEvent(event)
2381
James E. Blair3f876d52016-07-22 13:07:14 -07002382
2383class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002384 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002385 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002386
Joshua Heskethd78b4482015-09-14 16:56:34 -06002387
2388class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002389 def setup_config(self):
2390 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002391 for section_name in self.config.sections():
2392 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2393 section_name, re.I)
2394 if not con_match:
2395 continue
2396
2397 if self.config.get(section_name, 'driver') == 'sql':
2398 f = MySQLSchemaFixture()
2399 self.useFixture(f)
2400 if (self.config.get(section_name, 'dburi') ==
2401 '$MYSQL_FIXTURE_DBURI$'):
2402 self.config.set(section_name, 'dburi', f.dburi)