blob: 1092c6ea667d81e5c1e24340a63aaaec44836640 [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
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
92 return hashlib.sha1(str(random.random())).hexdigest()
93
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700136 categories = {'approved': ('Approved', -1, 1),
137 'code-review': ('Code-Review', -2, 2),
138 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700139
140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700141 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700176 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700183 ref = GerritChangeReference.create(
184 repo, '1/%s/%s' % (self.number, self.latest_patchset),
185 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700187 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.git.clean('-x', '-f', '-d')
189
190 path = os.path.join(self.upstream_root, self.project)
191 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for fn, content in files.items():
193 fn = os.path.join(path, fn)
194 with open(fn, 'w') as f:
195 f.write(content)
196 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700197 else:
198 for fni in range(100):
199 fn = os.path.join(path, str(fni))
200 f = open(fn, 'w')
201 for ci in range(4096):
202 f.write(random.choice(string.printable))
203 f.close()
204 repo.index.add([fn])
205
206 r = repo.index.commit(msg)
207 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700208 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700209 repo.git.clean('-x', '-f', '-d')
210 repo.heads['master'].checkout()
211 return r
212
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700214 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700216 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 data = ("test %s %s %s\n" %
218 (self.branch, self.number, self.latest_patchset))
219 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700220 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 ps_files = [{'file': '/COMMIT_MSG',
223 'type': 'ADDED'},
224 {'file': 'README',
225 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700227 ps_files.append({'file': f, 'type': 'ADDED'})
228 d = {'approvals': [],
229 'createdOn': time.time(),
230 'files': ps_files,
231 'number': str(self.latest_patchset),
232 'ref': 'refs/changes/1/%s/%s' % (self.number,
233 self.latest_patchset),
234 'revision': c.hexsha,
235 'uploader': {'email': 'user@example.com',
236 'name': 'User name',
237 'username': 'user'}}
238 self.data['currentPatchSet'] = d
239 self.patchsets.append(d)
240 self.data['submitRecords'] = self.getSubmitRecords()
241
242 def getPatchsetCreatedEvent(self, patchset):
243 event = {"type": "patchset-created",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "patchSet": self.patchsets[patchset - 1],
252 "uploader": {"name": "User Name"}}
253 return event
254
255 def getChangeRestoredEvent(self):
256 event = {"type": "change-restored",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100265 "patchSet": self.patchsets[-1],
266 "reason": ""}
267 return event
268
269 def getChangeAbandonedEvent(self):
270 event = {"type": "change-abandoned",
271 "change": {"project": self.project,
272 "branch": self.branch,
273 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
274 "number": str(self.number),
275 "subject": self.subject,
276 "owner": {"name": "User Name"},
277 "url": "https://hostname/3"},
278 "abandoner": {"name": "User Name"},
279 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700280 "reason": ""}
281 return event
282
283 def getChangeCommentEvent(self, patchset):
284 event = {"type": "comment-added",
285 "change": {"project": self.project,
286 "branch": self.branch,
287 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
288 "number": str(self.number),
289 "subject": self.subject,
290 "owner": {"name": "User Name"},
291 "url": "https://hostname/3"},
292 "patchSet": self.patchsets[patchset - 1],
293 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700294 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700295 "description": "Code-Review",
296 "value": "0"}],
297 "comment": "This is a comment"}
298 return event
299
James E. Blairc2a5ed72017-02-20 14:12:01 -0500300 def getChangeMergedEvent(self):
301 event = {"submitter": {"name": "Jenkins",
302 "username": "jenkins"},
303 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
304 "patchSet": self.patchsets[-1],
305 "change": self.data,
306 "type": "change-merged",
307 "eventCreatedOn": 1487613810}
308 return event
309
James E. Blair8cce42e2016-10-18 08:18:36 -0700310 def getRefUpdatedEvent(self):
311 path = os.path.join(self.upstream_root, self.project)
312 repo = git.Repo(path)
313 oldrev = repo.heads[self.branch].commit.hexsha
314
315 event = {
316 "type": "ref-updated",
317 "submitter": {
318 "name": "User Name",
319 },
320 "refUpdate": {
321 "oldRev": oldrev,
322 "newRev": self.patchsets[-1]['revision'],
323 "refName": self.branch,
324 "project": self.project,
325 }
326 }
327 return event
328
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 def addApproval(self, category, value, username='reviewer_john',
330 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700331 if not granted_on:
332 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000333 approval = {
334 'description': self.categories[category][0],
335 'type': category,
336 'value': str(value),
337 'by': {
338 'username': username,
339 'email': username + '@example.com',
340 },
341 'grantedOn': int(granted_on)
342 }
Clark Boylanb640e052014-04-03 16:41:46 -0700343 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
344 if x['by']['username'] == username and x['type'] == category:
345 del self.patchsets[-1]['approvals'][i]
346 self.patchsets[-1]['approvals'].append(approval)
347 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'author': {'email': 'author@example.com',
349 'name': 'Patchset Author',
350 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'change': {'branch': self.branch,
352 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
353 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'owner': {'email': 'owner@example.com',
355 'name': 'Change Owner',
356 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700357 'project': self.project,
358 'subject': self.subject,
359 'topic': 'master',
360 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'patchSet': self.patchsets[-1],
363 'type': 'comment-added'}
364 self.data['submitRecords'] = self.getSubmitRecords()
365 return json.loads(json.dumps(event))
366
367 def getSubmitRecords(self):
368 status = {}
369 for cat in self.categories.keys():
370 status[cat] = 0
371
372 for a in self.patchsets[-1]['approvals']:
373 cur = status[a['type']]
374 cat_min, cat_max = self.categories[a['type']][1:]
375 new = int(a['value'])
376 if new == cat_min:
377 cur = new
378 elif abs(new) > abs(cur):
379 cur = new
380 status[a['type']] = cur
381
382 labels = []
383 ok = True
384 for typ, cat in self.categories.items():
385 cur = status[typ]
386 cat_min, cat_max = cat[1:]
387 if cur == cat_min:
388 value = 'REJECT'
389 ok = False
390 elif cur == cat_max:
391 value = 'OK'
392 else:
393 value = 'NEED'
394 ok = False
395 labels.append({'label': cat[0], 'status': value})
396 if ok:
397 return [{'status': 'OK'}]
398 return [{'status': 'NOT_READY',
399 'labels': labels}]
400
401 def setDependsOn(self, other, patchset):
402 self.depends_on_change = other
403 d = {'id': other.data['id'],
404 'number': other.data['number'],
405 'ref': other.patchsets[patchset - 1]['ref']
406 }
407 self.data['dependsOn'] = [d]
408
409 other.needed_by_changes.append(self)
410 needed = other.data.get('neededBy', [])
411 d = {'id': self.data['id'],
412 'number': self.data['number'],
413 'ref': self.patchsets[patchset - 1]['ref'],
414 'revision': self.patchsets[patchset - 1]['revision']
415 }
416 needed.append(d)
417 other.data['neededBy'] = needed
418
419 def query(self):
420 self.queried += 1
421 d = self.data.get('dependsOn')
422 if d:
423 d = d[0]
424 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
425 d['isCurrentPatchSet'] = True
426 else:
427 d['isCurrentPatchSet'] = False
428 return json.loads(json.dumps(self.data))
429
430 def setMerged(self):
431 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000432 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700433 return
434 if self.fail_merge:
435 return
436 self.data['status'] = 'MERGED'
437 self.open = False
438
439 path = os.path.join(self.upstream_root, self.project)
440 repo = git.Repo(path)
441 repo.heads[self.branch].commit = \
442 repo.commit(self.patchsets[-1]['revision'])
443
444 def setReported(self):
445 self.reported += 1
446
447
James E. Blaire511d2f2016-12-08 15:22:26 -0800448class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700449 """A Fake Gerrit connection for use in tests.
450
451 This subclasses
452 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
453 ability for tests to add changes to the fake Gerrit it represents.
454 """
455
Joshua Hesketh352264b2015-08-11 23:42:08 +1000456 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700459 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000461 connection_config)
462
James E. Blair7fc8daa2016-08-08 15:37:15 -0700463 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700464 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
465 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200468 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700469
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def addFakeChange(self, project, branch, subject, status='NEW',
471 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700472 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700474 c = FakeGerritChange(self, self.change_number, project, branch,
475 subject, upstream_root=self.upstream_root,
476 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.changes[self.change_number] = c
478 return c
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480 def review(self, project, changeid, message, action):
481 number, ps = changeid.split(',')
482 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000483
484 # Add the approval back onto the change (ie simulate what gerrit would
485 # do).
486 # Usually when zuul leaves a review it'll create a feedback loop where
487 # zuul's review enters another gerrit event (which is then picked up by
488 # zuul). However, we can't mimic this behaviour (by adding this
489 # approval event into the queue) as it stops jobs from checking what
490 # happens before this event is triggered. If a job needs to see what
491 # happens they can add their own verified event into the queue.
492 # Nevertheless, we can update change with the new review in gerrit.
493
James E. Blair8b5408c2016-08-08 15:37:46 -0700494 for cat in action.keys():
495 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000496 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
James E. Blair8b5408c2016-08-08 15:37:46 -0700498 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499 if 'label' in action:
500 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000501 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000502
Clark Boylanb640e052014-04-03 16:41:46 -0700503 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000504
Clark Boylanb640e052014-04-03 16:41:46 -0700505 if 'submit' in action:
506 change.setMerged()
507 if message:
508 change.setReported()
509
510 def query(self, number):
511 change = self.changes.get(int(number))
512 if change:
513 return change.query()
514 return {}
515
James E. Blairc494d542014-08-06 09:23:52 -0700516 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700517 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700518 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800519 if query.startswith('change:'):
520 # Query a specific changeid
521 changeid = query[len('change:'):]
522 l = [change.query() for change in self.changes.values()
523 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700524 elif query.startswith('message:'):
525 # Query the content of a commit message
526 msg = query[len('message:'):].strip()
527 l = [change.query() for change in self.changes.values()
528 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800529 else:
530 # Query all open changes
531 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700532 return l
James E. Blairc494d542014-08-06 09:23:52 -0700533
Joshua Hesketh352264b2015-08-11 23:42:08 +1000534 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700535 pass
536
Joshua Hesketh352264b2015-08-11 23:42:08 +1000537 def getGitUrl(self, project):
538 return os.path.join(self.upstream_root, project.name)
539
Clark Boylanb640e052014-04-03 16:41:46 -0700540
Gregory Haynes4fc12542015-04-22 20:38:06 -0700541class GithubChangeReference(git.Reference):
542 _common_path_default = "refs/pull"
543 _points_to_commits_only = True
544
545
546class FakeGithubPullRequest(object):
547
548 def __init__(self, github, number, project, branch,
549 upstream_root, number_of_commits=1):
550 """Creates a new PR with several commits.
551 Sends an event about opened PR."""
552 self.github = github
553 self.source = github
554 self.number = number
555 self.project = project
556 self.branch = branch
557 self.upstream_root = upstream_root
558 self.comments = []
559 self.updated_at = None
560 self.head_sha = None
561 self._createPRRef()
562 self._addCommitToRepo()
563 self._updateTimeStamp()
564
565 def addCommit(self):
566 """Adds a commit on top of the actual PR head."""
567 self._addCommitToRepo()
568 self._updateTimeStamp()
569
570 def forcePush(self):
571 """Clears actual commits and add a commit on top of the base."""
572 self._addCommitToRepo(reset=True)
573 self._updateTimeStamp()
574
575 def getPullRequestOpenedEvent(self):
576 return self._getPullRequestEvent('opened')
577
578 def getPullRequestSynchronizeEvent(self):
579 return self._getPullRequestEvent('synchronize')
580
581 def getPullRequestReopenedEvent(self):
582 return self._getPullRequestEvent('reopened')
583
584 def getPullRequestClosedEvent(self):
585 return self._getPullRequestEvent('closed')
586
587 def addComment(self, message):
588 self.comments.append(message)
589 self._updateTimeStamp()
590
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200591 def getCommentAddedEvent(self, text):
592 name = 'issue_comment'
593 data = {
594 'action': 'created',
595 'issue': {
596 'number': self.number
597 },
598 'comment': {
599 'body': text
600 },
601 'repository': {
602 'full_name': self.project
603 }
604 }
605 return (name, data)
606
Gregory Haynes4fc12542015-04-22 20:38:06 -0700607 def _getRepo(self):
608 repo_path = os.path.join(self.upstream_root, self.project)
609 return git.Repo(repo_path)
610
611 def _createPRRef(self):
612 repo = self._getRepo()
613 GithubChangeReference.create(
614 repo, self._getPRReference(), 'refs/tags/init')
615
616 def _addCommitToRepo(self, reset=False):
617 repo = self._getRepo()
618 ref = repo.references[self._getPRReference()]
619 if reset:
620 ref.set_object('refs/tags/init')
621 repo.head.reference = ref
622 zuul.merger.merger.reset_repo_to_head(repo)
623 repo.git.clean('-x', '-f', '-d')
624
625 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
626 msg = 'test-%s' % self.number
627 fn = os.path.join(repo.working_dir, fn)
628 f = open(fn, 'w')
629 with open(fn, 'w') as f:
630 f.write("test %s %s\n" %
631 (self.branch, self.number))
632 repo.index.add([fn])
633
634 self.head_sha = repo.index.commit(msg).hexsha
635 repo.head.reference = 'master'
636 zuul.merger.merger.reset_repo_to_head(repo)
637 repo.git.clean('-x', '-f', '-d')
638 repo.heads['master'].checkout()
639
640 def _updateTimeStamp(self):
641 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
642
643 def getPRHeadSha(self):
644 repo = self._getRepo()
645 return repo.references[self._getPRReference()].commit.hexsha
646
647 def _getPRReference(self):
648 return '%s/head' % self.number
649
650 def _getPullRequestEvent(self, action):
651 name = 'pull_request'
652 data = {
653 'action': action,
654 'number': self.number,
655 'pull_request': {
656 'number': self.number,
657 'updated_at': self.updated_at,
658 'base': {
659 'ref': self.branch,
660 'repo': {
661 'full_name': self.project
662 }
663 },
664 'head': {
665 'sha': self.head_sha
666 }
667 }
668 }
669 return (name, data)
670
671
672class FakeGithubConnection(githubconnection.GithubConnection):
673 log = logging.getLogger("zuul.test.FakeGithubConnection")
674
675 def __init__(self, driver, connection_name, connection_config,
676 upstream_root=None):
677 super(FakeGithubConnection, self).__init__(driver, connection_name,
678 connection_config)
679 self.connection_name = connection_name
680 self.pr_number = 0
681 self.pull_requests = []
682 self.upstream_root = upstream_root
683
684 def openFakePullRequest(self, project, branch):
685 self.pr_number += 1
686 pull_request = FakeGithubPullRequest(
687 self, self.pr_number, project, branch, self.upstream_root)
688 self.pull_requests.append(pull_request)
689 return pull_request
690
Wayne1a78c612015-06-11 17:14:13 -0700691 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
692 if not old_rev:
693 old_rev = '00000000000000000000000000000000'
694 if not new_rev:
695 new_rev = random_sha1()
696 name = 'push'
697 data = {
698 'ref': ref,
699 'before': old_rev,
700 'after': new_rev,
701 'repository': {
702 'full_name': project
703 }
704 }
705 return (name, data)
706
Gregory Haynes4fc12542015-04-22 20:38:06 -0700707 def emitEvent(self, event):
708 """Emulates sending the GitHub webhook event to the connection."""
709 port = self.webapp.server.socket.getsockname()[1]
710 name, data = event
711 payload = json.dumps(data)
712 headers = {'X-Github-Event': name}
713 req = urllib.request.Request(
714 'http://localhost:%s/connection/%s/payload'
715 % (port, self.connection_name),
716 data=payload, headers=headers)
717 urllib.request.urlopen(req)
718
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200719 def getPull(self, project, number):
720 pr = self.pull_requests[number - 1]
721 data = {
722 'number': number,
723 'updated_at': pr.updated_at,
724 'base': {
725 'repo': {
726 'full_name': pr.project
727 },
728 'ref': pr.branch,
729 },
730 'head': {
731 'sha': pr.head_sha
732 }
733 }
734 return data
735
Gregory Haynes4fc12542015-04-22 20:38:06 -0700736 def getGitUrl(self, project):
737 return os.path.join(self.upstream_root, str(project))
738
739 def getProjectBranches(self, project):
740 """Masks getProjectBranches since we don't have a real github"""
741
742 # just returns master for now
743 return ['master']
744
Wayne40f40042015-06-12 16:56:30 -0700745 def report(self, project, pr_number, message, params=None):
746 pull_request = self.pull_requests[pr_number - 1]
747 pull_request.addComment(message)
748
Gregory Haynes4fc12542015-04-22 20:38:06 -0700749
Clark Boylanb640e052014-04-03 16:41:46 -0700750class BuildHistory(object):
751 def __init__(self, **kw):
752 self.__dict__.update(kw)
753
754 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700755 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
756 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700757
758
759class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200760 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700761 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700762 self.url = url
763
764 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700765 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700766 path = res.path
767 project = '/'.join(path.split('/')[2:-2])
768 ret = '001e# service=git-upload-pack\n'
769 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
770 'multi_ack thin-pack side-band side-band-64k ofs-delta '
771 'shallow no-progress include-tag multi_ack_detailed no-done\n')
772 path = os.path.join(self.upstream_root, project)
773 repo = git.Repo(path)
774 for ref in repo.refs:
775 r = ref.object.hexsha + ' ' + ref.path + '\n'
776 ret += '%04x%s' % (len(r) + 4, r)
777 ret += '0000'
778 return ret
779
780
Clark Boylanb640e052014-04-03 16:41:46 -0700781class FakeStatsd(threading.Thread):
782 def __init__(self):
783 threading.Thread.__init__(self)
784 self.daemon = True
785 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
786 self.sock.bind(('', 0))
787 self.port = self.sock.getsockname()[1]
788 self.wake_read, self.wake_write = os.pipe()
789 self.stats = []
790
791 def run(self):
792 while True:
793 poll = select.poll()
794 poll.register(self.sock, select.POLLIN)
795 poll.register(self.wake_read, select.POLLIN)
796 ret = poll.poll()
797 for (fd, event) in ret:
798 if fd == self.sock.fileno():
799 data = self.sock.recvfrom(1024)
800 if not data:
801 return
802 self.stats.append(data[0])
803 if fd == self.wake_read:
804 return
805
806 def stop(self):
807 os.write(self.wake_write, '1\n')
808
809
James E. Blaire1767bc2016-08-02 10:00:27 -0700810class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700811 log = logging.getLogger("zuul.test")
812
Paul Belanger174a8272017-03-14 13:20:10 -0400813 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700814 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400815 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700816 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700817 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700818 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700819 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700820 # TODOv3(jeblair): self.node is really "the image of the node
821 # assigned". We should rename it (self.node_image?) if we
822 # keep using it like this, or we may end up exposing more of
823 # the complexity around multi-node jobs here
824 # (self.nodes[0].image?)
825 self.node = None
826 if len(self.parameters.get('nodes')) == 1:
827 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700828 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100829 self.pipeline = self.parameters['ZUUL_PIPELINE']
830 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700831 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700832 self.wait_condition = threading.Condition()
833 self.waiting = False
834 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500835 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700836 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700837 self.changes = None
838 if 'ZUUL_CHANGE_IDS' in self.parameters:
839 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700840
James E. Blair3158e282016-08-19 09:34:11 -0700841 def __repr__(self):
842 waiting = ''
843 if self.waiting:
844 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100845 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
846 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700847
Clark Boylanb640e052014-04-03 16:41:46 -0700848 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700849 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700850 self.wait_condition.acquire()
851 self.wait_condition.notify()
852 self.waiting = False
853 self.log.debug("Build %s released" % self.unique)
854 self.wait_condition.release()
855
856 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700857 """Return whether this build is being held.
858
859 :returns: Whether the build is being held.
860 :rtype: bool
861 """
862
Clark Boylanb640e052014-04-03 16:41:46 -0700863 self.wait_condition.acquire()
864 if self.waiting:
865 ret = True
866 else:
867 ret = False
868 self.wait_condition.release()
869 return ret
870
871 def _wait(self):
872 self.wait_condition.acquire()
873 self.waiting = True
874 self.log.debug("Build %s waiting" % self.unique)
875 self.wait_condition.wait()
876 self.wait_condition.release()
877
878 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700879 self.log.debug('Running build %s' % self.unique)
880
Paul Belanger174a8272017-03-14 13:20:10 -0400881 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700882 self.log.debug('Holding build %s' % self.unique)
883 self._wait()
884 self.log.debug("Build %s continuing" % self.unique)
885
James E. Blair412fba82017-01-26 15:00:50 -0800886 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700887 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800888 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700889 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800890 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500891 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800892 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700893
James E. Blaire1767bc2016-08-02 10:00:27 -0700894 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700895
James E. Blaira5dba232016-08-08 15:53:24 -0700896 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400897 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700898 for change in changes:
899 if self.hasChanges(change):
900 return True
901 return False
902
James E. Blaire7b99a02016-08-05 14:27:34 -0700903 def hasChanges(self, *changes):
904 """Return whether this build has certain changes in its git repos.
905
906 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700907 are expected to be present (in order) in the git repository of
908 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700909
910 :returns: Whether the build has the indicated changes.
911 :rtype: bool
912
913 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800914 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -0700915 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -0700916 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800917 try:
918 repo = git.Repo(path)
919 except NoSuchPathError as e:
920 self.log.debug('%s' % e)
921 return False
922 ref = self.parameters['ZUUL_REF']
923 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
924 commit_message = '%s-1' % change.subject
925 self.log.debug("Checking if build %s has changes; commit_message "
926 "%s; repo_messages %s" % (self, commit_message,
927 repo_messages))
928 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700929 self.log.debug(" messages do not match")
930 return False
931 self.log.debug(" OK")
932 return True
933
Clark Boylanb640e052014-04-03 16:41:46 -0700934
Paul Belanger174a8272017-03-14 13:20:10 -0400935class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
936 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700937
Paul Belanger174a8272017-03-14 13:20:10 -0400938 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700939 they will report that they have started but then pause until
940 released before reporting completion. This attribute may be
941 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400942 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700943 be explicitly released.
944
945 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800946 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700947 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800948 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400949 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700950 self.hold_jobs_in_build = False
951 self.lock = threading.Lock()
952 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700953 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700954 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700955 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800956
James E. Blaira5dba232016-08-08 15:53:24 -0700957 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400958 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700959
960 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700961 :arg Change change: The :py:class:`~tests.base.FakeChange`
962 instance which should cause the job to fail. This job
963 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700964
965 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700966 l = self.fail_tests.get(name, [])
967 l.append(change)
968 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800969
James E. Blair962220f2016-08-03 11:22:38 -0700970 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700971 """Release a held build.
972
973 :arg str regex: A regular expression which, if supplied, will
974 cause only builds with matching names to be released. If
975 not supplied, all builds will be released.
976
977 """
James E. Blair962220f2016-08-03 11:22:38 -0700978 builds = self.running_builds[:]
979 self.log.debug("Releasing build %s (%s)" % (regex,
980 len(self.running_builds)))
981 for build in builds:
982 if not regex or re.match(regex, build.name):
983 self.log.debug("Releasing build %s" %
984 (build.parameters['ZUUL_UUID']))
985 build.release()
986 else:
987 self.log.debug("Not releasing build %s" %
988 (build.parameters['ZUUL_UUID']))
989 self.log.debug("Done releasing builds %s (%s)" %
990 (regex, len(self.running_builds)))
991
Paul Belanger174a8272017-03-14 13:20:10 -0400992 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700993 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700994 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700995 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700996 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800997 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500998 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800999 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001000 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1001 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001002
1003 def stopJob(self, job):
1004 self.log.debug("handle stop")
1005 parameters = json.loads(job.arguments)
1006 uuid = parameters['uuid']
1007 for build in self.running_builds:
1008 if build.unique == uuid:
1009 build.aborted = True
1010 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001011 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001012
James E. Blaira002b032017-04-18 10:35:48 -07001013 def stop(self):
1014 for build in self.running_builds:
1015 build.release()
1016 super(RecordingExecutorServer, self).stop()
1017
Joshua Hesketh50c21782016-10-13 21:34:14 +11001018
Paul Belanger174a8272017-03-14 13:20:10 -04001019class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001020 def doMergeChanges(self, items):
1021 # Get a merger in order to update the repos involved in this job.
1022 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1023 if not commit: # merge conflict
1024 self.recordResult('MERGER_FAILURE')
1025 return commit
1026
1027 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001028 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001029 self.executor_server.lock.acquire()
1030 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001031 BuildHistory(name=build.name, result=result, changes=build.changes,
1032 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001033 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001034 pipeline=build.parameters['ZUUL_PIPELINE'])
1035 )
Paul Belanger174a8272017-03-14 13:20:10 -04001036 self.executor_server.running_builds.remove(build)
1037 del self.executor_server.job_builds[self.job.unique]
1038 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001039
1040 def runPlaybooks(self, args):
1041 build = self.executor_server.job_builds[self.job.unique]
1042 build.jobdir = self.jobdir
1043
1044 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1045 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001046 return result
1047
Monty Taylore6562aa2017-02-20 07:37:39 -05001048 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001049 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001050
Paul Belanger174a8272017-03-14 13:20:10 -04001051 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001052 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001053 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001054 else:
1055 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001056 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001057
James E. Blairad8dca02017-02-21 11:48:32 -05001058 def getHostList(self, args):
1059 self.log.debug("hostlist")
1060 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001061 for host in hosts:
1062 host['host_vars']['ansible_connection'] = 'local'
1063
1064 hosts.append(dict(
1065 name='localhost',
1066 host_vars=dict(ansible_connection='local'),
1067 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001068 return hosts
1069
James E. Blairf5dbd002015-12-23 15:26:17 -08001070
Clark Boylanb640e052014-04-03 16:41:46 -07001071class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001072 """A Gearman server for use in tests.
1073
1074 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1075 added to the queue but will not be distributed to workers
1076 until released. This attribute may be changed at any time and
1077 will take effect for subsequently enqueued jobs, but
1078 previously held jobs will still need to be explicitly
1079 released.
1080
1081 """
1082
Clark Boylanb640e052014-04-03 16:41:46 -07001083 def __init__(self):
1084 self.hold_jobs_in_queue = False
1085 super(FakeGearmanServer, self).__init__(0)
1086
1087 def getJobForConnection(self, connection, peek=False):
1088 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1089 for job in queue:
1090 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001091 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001092 job.waiting = self.hold_jobs_in_queue
1093 else:
1094 job.waiting = False
1095 if job.waiting:
1096 continue
1097 if job.name in connection.functions:
1098 if not peek:
1099 queue.remove(job)
1100 connection.related_jobs[job.handle] = job
1101 job.worker_connection = connection
1102 job.running = True
1103 return job
1104 return None
1105
1106 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001107 """Release a held job.
1108
1109 :arg str regex: A regular expression which, if supplied, will
1110 cause only jobs with matching names to be released. If
1111 not supplied, all jobs will be released.
1112 """
Clark Boylanb640e052014-04-03 16:41:46 -07001113 released = False
1114 qlen = (len(self.high_queue) + len(self.normal_queue) +
1115 len(self.low_queue))
1116 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1117 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001118 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001119 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001120 parameters = json.loads(job.arguments)
1121 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.log.debug("releasing queued job %s" %
1123 job.unique)
1124 job.waiting = False
1125 released = True
1126 else:
1127 self.log.debug("not releasing queued job %s" %
1128 job.unique)
1129 if released:
1130 self.wakeConnections()
1131 qlen = (len(self.high_queue) + len(self.normal_queue) +
1132 len(self.low_queue))
1133 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1134
1135
1136class FakeSMTP(object):
1137 log = logging.getLogger('zuul.FakeSMTP')
1138
1139 def __init__(self, messages, server, port):
1140 self.server = server
1141 self.port = port
1142 self.messages = messages
1143
1144 def sendmail(self, from_email, to_email, msg):
1145 self.log.info("Sending email from %s, to %s, with msg %s" % (
1146 from_email, to_email, msg))
1147
1148 headers = msg.split('\n\n', 1)[0]
1149 body = msg.split('\n\n', 1)[1]
1150
1151 self.messages.append(dict(
1152 from_email=from_email,
1153 to_email=to_email,
1154 msg=msg,
1155 headers=headers,
1156 body=body,
1157 ))
1158
1159 return True
1160
1161 def quit(self):
1162 return True
1163
1164
James E. Blairdce6cea2016-12-20 16:45:32 -08001165class FakeNodepool(object):
1166 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001167 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001168
1169 log = logging.getLogger("zuul.test.FakeNodepool")
1170
1171 def __init__(self, host, port, chroot):
1172 self.client = kazoo.client.KazooClient(
1173 hosts='%s:%s%s' % (host, port, chroot))
1174 self.client.start()
1175 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001176 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001177 self.thread = threading.Thread(target=self.run)
1178 self.thread.daemon = True
1179 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001180 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001181
1182 def stop(self):
1183 self._running = False
1184 self.thread.join()
1185 self.client.stop()
1186 self.client.close()
1187
1188 def run(self):
1189 while self._running:
1190 self._run()
1191 time.sleep(0.1)
1192
1193 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001194 if self.paused:
1195 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001196 for req in self.getNodeRequests():
1197 self.fulfillRequest(req)
1198
1199 def getNodeRequests(self):
1200 try:
1201 reqids = self.client.get_children(self.REQUEST_ROOT)
1202 except kazoo.exceptions.NoNodeError:
1203 return []
1204 reqs = []
1205 for oid in sorted(reqids):
1206 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001207 try:
1208 data, stat = self.client.get(path)
1209 data = json.loads(data)
1210 data['_oid'] = oid
1211 reqs.append(data)
1212 except kazoo.exceptions.NoNodeError:
1213 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001214 return reqs
1215
James E. Blaire18d4602017-01-05 11:17:28 -08001216 def getNodes(self):
1217 try:
1218 nodeids = self.client.get_children(self.NODE_ROOT)
1219 except kazoo.exceptions.NoNodeError:
1220 return []
1221 nodes = []
1222 for oid in sorted(nodeids):
1223 path = self.NODE_ROOT + '/' + oid
1224 data, stat = self.client.get(path)
1225 data = json.loads(data)
1226 data['_oid'] = oid
1227 try:
1228 lockfiles = self.client.get_children(path + '/lock')
1229 except kazoo.exceptions.NoNodeError:
1230 lockfiles = []
1231 if lockfiles:
1232 data['_lock'] = True
1233 else:
1234 data['_lock'] = False
1235 nodes.append(data)
1236 return nodes
1237
James E. Blaira38c28e2017-01-04 10:33:20 -08001238 def makeNode(self, request_id, node_type):
1239 now = time.time()
1240 path = '/nodepool/nodes/'
1241 data = dict(type=node_type,
1242 provider='test-provider',
1243 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001244 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001245 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001246 public_ipv4='127.0.0.1',
1247 private_ipv4=None,
1248 public_ipv6=None,
1249 allocated_to=request_id,
1250 state='ready',
1251 state_time=now,
1252 created_time=now,
1253 updated_time=now,
1254 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001255 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001256 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001257 data = json.dumps(data)
1258 path = self.client.create(path, data,
1259 makepath=True,
1260 sequence=True)
1261 nodeid = path.split("/")[-1]
1262 return nodeid
1263
James E. Blair6ab79e02017-01-06 10:10:17 -08001264 def addFailRequest(self, request):
1265 self.fail_requests.add(request['_oid'])
1266
James E. Blairdce6cea2016-12-20 16:45:32 -08001267 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001268 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001269 return
1270 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001271 oid = request['_oid']
1272 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001273
James E. Blair6ab79e02017-01-06 10:10:17 -08001274 if oid in self.fail_requests:
1275 request['state'] = 'failed'
1276 else:
1277 request['state'] = 'fulfilled'
1278 nodes = []
1279 for node in request['node_types']:
1280 nodeid = self.makeNode(oid, node)
1281 nodes.append(nodeid)
1282 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001283
James E. Blaira38c28e2017-01-04 10:33:20 -08001284 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001285 path = self.REQUEST_ROOT + '/' + oid
1286 data = json.dumps(request)
1287 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1288 self.client.set(path, data)
1289
1290
James E. Blair498059b2016-12-20 13:50:13 -08001291class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001292 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001293 super(ChrootedKazooFixture, self).__init__()
1294
1295 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1296 if ':' in zk_host:
1297 host, port = zk_host.split(':')
1298 else:
1299 host = zk_host
1300 port = None
1301
1302 self.zookeeper_host = host
1303
1304 if not port:
1305 self.zookeeper_port = 2181
1306 else:
1307 self.zookeeper_port = int(port)
1308
Clark Boylan621ec9a2017-04-07 17:41:33 -07001309 self.test_id = test_id
1310
James E. Blair498059b2016-12-20 13:50:13 -08001311 def _setUp(self):
1312 # Make sure the test chroot paths do not conflict
1313 random_bits = ''.join(random.choice(string.ascii_lowercase +
1314 string.ascii_uppercase)
1315 for x in range(8))
1316
Clark Boylan621ec9a2017-04-07 17:41:33 -07001317 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001318 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1319
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001320 self.addCleanup(self._cleanup)
1321
James E. Blair498059b2016-12-20 13:50:13 -08001322 # Ensure the chroot path exists and clean up any pre-existing znodes.
1323 _tmp_client = kazoo.client.KazooClient(
1324 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1325 _tmp_client.start()
1326
1327 if _tmp_client.exists(self.zookeeper_chroot):
1328 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1329
1330 _tmp_client.ensure_path(self.zookeeper_chroot)
1331 _tmp_client.stop()
1332 _tmp_client.close()
1333
James E. Blair498059b2016-12-20 13:50:13 -08001334 def _cleanup(self):
1335 '''Remove the chroot path.'''
1336 # Need a non-chroot'ed client to remove the chroot path
1337 _tmp_client = kazoo.client.KazooClient(
1338 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1339 _tmp_client.start()
1340 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1341 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001342 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001343
1344
Joshua Heskethd78b4482015-09-14 16:56:34 -06001345class MySQLSchemaFixture(fixtures.Fixture):
1346 def setUp(self):
1347 super(MySQLSchemaFixture, self).setUp()
1348
1349 random_bits = ''.join(random.choice(string.ascii_lowercase +
1350 string.ascii_uppercase)
1351 for x in range(8))
1352 self.name = '%s_%s' % (random_bits, os.getpid())
1353 self.passwd = uuid.uuid4().hex
1354 db = pymysql.connect(host="localhost",
1355 user="openstack_citest",
1356 passwd="openstack_citest",
1357 db="openstack_citest")
1358 cur = db.cursor()
1359 cur.execute("create database %s" % self.name)
1360 cur.execute(
1361 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1362 (self.name, self.name, self.passwd))
1363 cur.execute("flush privileges")
1364
1365 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1366 self.passwd,
1367 self.name)
1368 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1369 self.addCleanup(self.cleanup)
1370
1371 def cleanup(self):
1372 db = pymysql.connect(host="localhost",
1373 user="openstack_citest",
1374 passwd="openstack_citest",
1375 db="openstack_citest")
1376 cur = db.cursor()
1377 cur.execute("drop database %s" % self.name)
1378 cur.execute("drop user '%s'@'localhost'" % self.name)
1379 cur.execute("flush privileges")
1380
1381
Maru Newby3fe5f852015-01-13 04:22:14 +00001382class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001383 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001384 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001385
James E. Blair1c236df2017-02-01 14:07:24 -08001386 def attachLogs(self, *args):
1387 def reader():
1388 self._log_stream.seek(0)
1389 while True:
1390 x = self._log_stream.read(4096)
1391 if not x:
1392 break
1393 yield x.encode('utf8')
1394 content = testtools.content.content_from_reader(
1395 reader,
1396 testtools.content_type.UTF8_TEXT,
1397 False)
1398 self.addDetail('logging', content)
1399
Clark Boylanb640e052014-04-03 16:41:46 -07001400 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001401 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001402 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1403 try:
1404 test_timeout = int(test_timeout)
1405 except ValueError:
1406 # If timeout value is invalid do not set a timeout.
1407 test_timeout = 0
1408 if test_timeout > 0:
1409 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1410
1411 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1412 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1413 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1414 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1415 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1416 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1417 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1418 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1419 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1420 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001421 self._log_stream = StringIO()
1422 self.addOnException(self.attachLogs)
1423 else:
1424 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001425
James E. Blair1c236df2017-02-01 14:07:24 -08001426 handler = logging.StreamHandler(self._log_stream)
1427 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1428 '%(levelname)-8s %(message)s')
1429 handler.setFormatter(formatter)
1430
1431 logger = logging.getLogger()
1432 logger.setLevel(logging.DEBUG)
1433 logger.addHandler(handler)
1434
Clark Boylan3410d532017-04-25 12:35:29 -07001435 # Make sure we don't carry old handlers around in process state
1436 # which slows down test runs
1437 self.addCleanup(logger.removeHandler, handler)
1438 self.addCleanup(handler.close)
1439 self.addCleanup(handler.flush)
1440
James E. Blair1c236df2017-02-01 14:07:24 -08001441 # NOTE(notmorgan): Extract logging overrides for specific
1442 # libraries from the OS_LOG_DEFAULTS env and create loggers
1443 # for each. This is used to limit the output during test runs
1444 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001445 log_defaults_from_env = os.environ.get(
1446 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001447 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001448
James E. Blairdce6cea2016-12-20 16:45:32 -08001449 if log_defaults_from_env:
1450 for default in log_defaults_from_env.split(','):
1451 try:
1452 name, level_str = default.split('=', 1)
1453 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001454 logger = logging.getLogger(name)
1455 logger.setLevel(level)
1456 logger.addHandler(handler)
1457 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001458 except ValueError:
1459 # NOTE(notmorgan): Invalid format of the log default,
1460 # skip and don't try and apply a logger for the
1461 # specified module
1462 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001463
Maru Newby3fe5f852015-01-13 04:22:14 +00001464
1465class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001466 """A test case with a functioning Zuul.
1467
1468 The following class variables are used during test setup and can
1469 be overidden by subclasses but are effectively read-only once a
1470 test method starts running:
1471
1472 :cvar str config_file: This points to the main zuul config file
1473 within the fixtures directory. Subclasses may override this
1474 to obtain a different behavior.
1475
1476 :cvar str tenant_config_file: This is the tenant config file
1477 (which specifies from what git repos the configuration should
1478 be loaded). It defaults to the value specified in
1479 `config_file` but can be overidden by subclasses to obtain a
1480 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001481 configuration. See also the :py:func:`simple_layout`
1482 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001483
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001484 :cvar bool create_project_keys: Indicates whether Zuul should
1485 auto-generate keys for each project, or whether the test
1486 infrastructure should insert dummy keys to save time during
1487 startup. Defaults to False.
1488
James E. Blaire7b99a02016-08-05 14:27:34 -07001489 The following are instance variables that are useful within test
1490 methods:
1491
1492 :ivar FakeGerritConnection fake_<connection>:
1493 A :py:class:`~tests.base.FakeGerritConnection` will be
1494 instantiated for each connection present in the config file
1495 and stored here. For instance, `fake_gerrit` will hold the
1496 FakeGerritConnection object for a connection named `gerrit`.
1497
1498 :ivar FakeGearmanServer gearman_server: An instance of
1499 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1500 server that all of the Zuul components in this test use to
1501 communicate with each other.
1502
Paul Belanger174a8272017-03-14 13:20:10 -04001503 :ivar RecordingExecutorServer executor_server: An instance of
1504 :py:class:`~tests.base.RecordingExecutorServer` which is the
1505 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001506
1507 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1508 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001509 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001510 list upon completion.
1511
1512 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1513 objects representing completed builds. They are appended to
1514 the list in the order they complete.
1515
1516 """
1517
James E. Blair83005782015-12-11 14:46:03 -08001518 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001519 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001520 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001521
1522 def _startMerger(self):
1523 self.merge_server = zuul.merger.server.MergeServer(self.config,
1524 self.connections)
1525 self.merge_server.start()
1526
Maru Newby3fe5f852015-01-13 04:22:14 +00001527 def setUp(self):
1528 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001529
1530 self.setupZK()
1531
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001532 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001533 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001534 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1535 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001536 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001537 tmp_root = tempfile.mkdtemp(
1538 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001539 self.test_root = os.path.join(tmp_root, "zuul-test")
1540 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001541 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001542 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001543 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001544
1545 if os.path.exists(self.test_root):
1546 shutil.rmtree(self.test_root)
1547 os.makedirs(self.test_root)
1548 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001549 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001550
1551 # Make per test copy of Configuration.
1552 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001553 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001554 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001555 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001556 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001557 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001558 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001559
Clark Boylanb640e052014-04-03 16:41:46 -07001560 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001561 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1562 # see: https://github.com/jsocol/pystatsd/issues/61
1563 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001564 os.environ['STATSD_PORT'] = str(self.statsd.port)
1565 self.statsd.start()
1566 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001567 reload_module(statsd)
1568 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001569
1570 self.gearman_server = FakeGearmanServer()
1571
1572 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001573 self.log.info("Gearman server on port %s" %
1574 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001575
James E. Blaire511d2f2016-12-08 15:22:26 -08001576 gerritsource.GerritSource.replication_timeout = 1.5
1577 gerritsource.GerritSource.replication_retry_interval = 0.5
1578 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001579
Joshua Hesketh352264b2015-08-11 23:42:08 +10001580 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001581
Jan Hruban7083edd2015-08-21 14:00:54 +02001582 self.webapp = zuul.webapp.WebApp(
1583 self.sched, port=0, listen_address='127.0.0.1')
1584
Jan Hruban6b71aff2015-10-22 16:58:08 +02001585 self.event_queues = [
1586 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001587 self.sched.trigger_event_queue,
1588 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001589 ]
1590
James E. Blairfef78942016-03-11 16:28:56 -08001591 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001592 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001593
Clark Boylanb640e052014-04-03 16:41:46 -07001594 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001595 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001596 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001597 return FakeURLOpener(self.upstream_root, *args, **kw)
1598
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001599 old_urlopen = urllib.request.urlopen
1600 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001601
James E. Blair3f876d52016-07-22 13:07:14 -07001602 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001603
Paul Belanger174a8272017-03-14 13:20:10 -04001604 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001605 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001606 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001607 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001608 _test_root=self.test_root,
1609 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001610 self.executor_server.start()
1611 self.history = self.executor_server.build_history
1612 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001613
Paul Belanger174a8272017-03-14 13:20:10 -04001614 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001615 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001616 self.merge_client = zuul.merger.client.MergeClient(
1617 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001618 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001619 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001620 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001621
James E. Blair0d5a36e2017-02-21 10:53:44 -05001622 self.fake_nodepool = FakeNodepool(
1623 self.zk_chroot_fixture.zookeeper_host,
1624 self.zk_chroot_fixture.zookeeper_port,
1625 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001626
Paul Belanger174a8272017-03-14 13:20:10 -04001627 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001628 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001629 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001630 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001631
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001632 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001633
1634 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001635 self.webapp.start()
1636 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001637 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001638 # Cleanups are run in reverse order
1639 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001640 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001641 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001642
James E. Blairb9c0d772017-03-03 14:34:49 -08001643 self.sched.reconfigure(self.config)
1644 self.sched.resume()
1645
James E. Blairfef78942016-03-11 16:28:56 -08001646 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001647 # Set up gerrit related fakes
1648 # Set a changes database so multiple FakeGerrit's can report back to
1649 # a virtual canonical database given by the configured hostname
1650 self.gerrit_changes_dbs = {}
1651
1652 def getGerritConnection(driver, name, config):
1653 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1654 con = FakeGerritConnection(driver, name, config,
1655 changes_db=db,
1656 upstream_root=self.upstream_root)
1657 self.event_queues.append(con.event_queue)
1658 setattr(self, 'fake_' + name, con)
1659 return con
1660
1661 self.useFixture(fixtures.MonkeyPatch(
1662 'zuul.driver.gerrit.GerritDriver.getConnection',
1663 getGerritConnection))
1664
Gregory Haynes4fc12542015-04-22 20:38:06 -07001665 def getGithubConnection(driver, name, config):
1666 con = FakeGithubConnection(driver, name, config,
1667 upstream_root=self.upstream_root)
1668 setattr(self, 'fake_' + name, con)
1669 return con
1670
1671 self.useFixture(fixtures.MonkeyPatch(
1672 'zuul.driver.github.GithubDriver.getConnection',
1673 getGithubConnection))
1674
James E. Blaire511d2f2016-12-08 15:22:26 -08001675 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001676 # TODO(jhesketh): This should come from lib.connections for better
1677 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001678 # Register connections from the config
1679 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001680
Joshua Hesketh352264b2015-08-11 23:42:08 +10001681 def FakeSMTPFactory(*args, **kw):
1682 args = [self.smtp_messages] + list(args)
1683 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001684
Joshua Hesketh352264b2015-08-11 23:42:08 +10001685 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001686
James E. Blaire511d2f2016-12-08 15:22:26 -08001687 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001688 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001689 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001690
James E. Blair83005782015-12-11 14:46:03 -08001691 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001692 # This creates the per-test configuration object. It can be
1693 # overriden by subclasses, but should not need to be since it
1694 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001695 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001696 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001697
1698 if not self.setupSimpleLayout():
1699 if hasattr(self, 'tenant_config_file'):
1700 self.config.set('zuul', 'tenant_config',
1701 self.tenant_config_file)
1702 git_path = os.path.join(
1703 os.path.dirname(
1704 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1705 'git')
1706 if os.path.exists(git_path):
1707 for reponame in os.listdir(git_path):
1708 project = reponame.replace('_', '/')
1709 self.copyDirToRepo(project,
1710 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001711 self.setupAllProjectKeys()
1712
James E. Blair06cc3922017-04-19 10:08:10 -07001713 def setupSimpleLayout(self):
1714 # If the test method has been decorated with a simple_layout,
1715 # use that instead of the class tenant_config_file. Set up a
1716 # single config-project with the specified layout, and
1717 # initialize repos for all of the 'project' entries which
1718 # appear in the layout.
1719 test_name = self.id().split('.')[-1]
1720 test = getattr(self, test_name)
1721 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001722 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001723 else:
1724 return False
1725
James E. Blairb70e55a2017-04-19 12:57:02 -07001726 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001727 path = os.path.join(FIXTURE_DIR, path)
1728 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001729 data = f.read()
1730 layout = yaml.safe_load(data)
1731 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001732 untrusted_projects = []
1733 for item in layout:
1734 if 'project' in item:
1735 name = item['project']['name']
1736 untrusted_projects.append(name)
1737 self.init_repo(name)
1738 self.addCommitToRepo(name, 'initial commit',
1739 files={'README': ''},
1740 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001741 if 'job' in item:
1742 jobname = item['job']['name']
1743 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001744
1745 root = os.path.join(self.test_root, "config")
1746 if not os.path.exists(root):
1747 os.makedirs(root)
1748 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1749 config = [{'tenant':
1750 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001751 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001752 {'config-projects': ['common-config'],
1753 'untrusted-projects': untrusted_projects}}}}]
1754 f.write(yaml.dump(config))
1755 f.close()
1756 self.config.set('zuul', 'tenant_config',
1757 os.path.join(FIXTURE_DIR, f.name))
1758
1759 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001760 self.addCommitToRepo('common-config', 'add content from fixture',
1761 files, branch='master', tag='init')
1762
1763 return True
1764
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001765 def setupAllProjectKeys(self):
1766 if self.create_project_keys:
1767 return
1768
1769 path = self.config.get('zuul', 'tenant_config')
1770 with open(os.path.join(FIXTURE_DIR, path)) as f:
1771 tenant_config = yaml.safe_load(f.read())
1772 for tenant in tenant_config:
1773 sources = tenant['tenant']['source']
1774 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001775 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001776 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001777 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001778 self.setupProjectKeys(source, project)
1779
1780 def setupProjectKeys(self, source, project):
1781 # Make sure we set up an RSA key for the project so that we
1782 # don't spend time generating one:
1783
1784 key_root = os.path.join(self.state_root, 'keys')
1785 if not os.path.isdir(key_root):
1786 os.mkdir(key_root, 0o700)
1787 private_key_file = os.path.join(key_root, source, project + '.pem')
1788 private_key_dir = os.path.dirname(private_key_file)
1789 self.log.debug("Installing test keys for project %s at %s" % (
1790 project, private_key_file))
1791 if not os.path.isdir(private_key_dir):
1792 os.makedirs(private_key_dir)
1793 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1794 with open(private_key_file, 'w') as o:
1795 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001796
James E. Blair498059b2016-12-20 13:50:13 -08001797 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001798 self.zk_chroot_fixture = self.useFixture(
1799 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001800 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001801 self.zk_chroot_fixture.zookeeper_host,
1802 self.zk_chroot_fixture.zookeeper_port,
1803 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001804
James E. Blair96c6bf82016-01-15 16:20:40 -08001805 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001806 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001807
1808 files = {}
1809 for (dirpath, dirnames, filenames) in os.walk(source_path):
1810 for filename in filenames:
1811 test_tree_filepath = os.path.join(dirpath, filename)
1812 common_path = os.path.commonprefix([test_tree_filepath,
1813 source_path])
1814 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1815 with open(test_tree_filepath, 'r') as f:
1816 content = f.read()
1817 files[relative_filepath] = content
1818 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001819 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001820
James E. Blaire18d4602017-01-05 11:17:28 -08001821 def assertNodepoolState(self):
1822 # Make sure that there are no pending requests
1823
1824 requests = self.fake_nodepool.getNodeRequests()
1825 self.assertEqual(len(requests), 0)
1826
1827 nodes = self.fake_nodepool.getNodes()
1828 for node in nodes:
1829 self.assertFalse(node['_lock'], "Node %s is locked" %
1830 (node['_oid'],))
1831
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001832 def assertNoGeneratedKeys(self):
1833 # Make sure that Zuul did not generate any project keys
1834 # (unless it was supposed to).
1835
1836 if self.create_project_keys:
1837 return
1838
1839 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1840 test_key = i.read()
1841
1842 key_root = os.path.join(self.state_root, 'keys')
1843 for root, dirname, files in os.walk(key_root):
1844 for fn in files:
1845 with open(os.path.join(root, fn)) as f:
1846 self.assertEqual(test_key, f.read())
1847
Clark Boylanb640e052014-04-03 16:41:46 -07001848 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001849 self.log.debug("Assert final state")
1850 # Make sure no jobs are running
1851 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001852 # Make sure that git.Repo objects have been garbage collected.
1853 repos = []
1854 gc.collect()
1855 for obj in gc.get_objects():
1856 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001857 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001858 repos.append(obj)
1859 self.assertEqual(len(repos), 0)
1860 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001861 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001862 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001863 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001864 for tenant in self.sched.abide.tenants.values():
1865 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001866 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001867 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001868
1869 def shutdown(self):
1870 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001871 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001872 self.merge_server.stop()
1873 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001874 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001875 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001876 self.sched.stop()
1877 self.sched.join()
1878 self.statsd.stop()
1879 self.statsd.join()
1880 self.webapp.stop()
1881 self.webapp.join()
1882 self.rpc.stop()
1883 self.rpc.join()
1884 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001885 self.fake_nodepool.stop()
1886 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001887 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001888 # we whitelist watchdog threads as they have relatively long delays
1889 # before noticing they should exit, but they should exit on their own.
1890 threads = [t for t in threading.enumerate()
1891 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001892 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001893 log_str = ""
1894 for thread_id, stack_frame in sys._current_frames().items():
1895 log_str += "Thread: %s\n" % thread_id
1896 log_str += "".join(traceback.format_stack(stack_frame))
1897 self.log.debug(log_str)
1898 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001899
James E. Blaira002b032017-04-18 10:35:48 -07001900 def assertCleanShutdown(self):
1901 pass
1902
James E. Blairc4ba97a2017-04-19 16:26:24 -07001903 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001904 parts = project.split('/')
1905 path = os.path.join(self.upstream_root, *parts[:-1])
1906 if not os.path.exists(path):
1907 os.makedirs(path)
1908 path = os.path.join(self.upstream_root, project)
1909 repo = git.Repo.init(path)
1910
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001911 with repo.config_writer() as config_writer:
1912 config_writer.set_value('user', 'email', 'user@example.com')
1913 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001914
Clark Boylanb640e052014-04-03 16:41:46 -07001915 repo.index.commit('initial commit')
1916 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001917 if tag:
1918 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001919
James E. Blair97d902e2014-08-21 13:25:56 -07001920 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001921 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001922 repo.git.clean('-x', '-f', '-d')
1923
James E. Blair97d902e2014-08-21 13:25:56 -07001924 def create_branch(self, project, branch):
1925 path = os.path.join(self.upstream_root, project)
1926 repo = git.Repo.init(path)
1927 fn = os.path.join(path, 'README')
1928
1929 branch_head = repo.create_head(branch)
1930 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001931 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001932 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001933 f.close()
1934 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001935 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001936
James E. Blair97d902e2014-08-21 13:25:56 -07001937 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001938 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001939 repo.git.clean('-x', '-f', '-d')
1940
Sachi King9f16d522016-03-16 12:20:45 +11001941 def create_commit(self, project):
1942 path = os.path.join(self.upstream_root, project)
1943 repo = git.Repo(path)
1944 repo.head.reference = repo.heads['master']
1945 file_name = os.path.join(path, 'README')
1946 with open(file_name, 'a') as f:
1947 f.write('creating fake commit\n')
1948 repo.index.add([file_name])
1949 commit = repo.index.commit('Creating a fake commit')
1950 return commit.hexsha
1951
James E. Blairf4a5f022017-04-18 14:01:10 -07001952 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001953 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001954 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001955 while len(self.builds):
1956 self.release(self.builds[0])
1957 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001958 i += 1
1959 if count is not None and i >= count:
1960 break
James E. Blairb8c16472015-05-05 14:55:26 -07001961
Clark Boylanb640e052014-04-03 16:41:46 -07001962 def release(self, job):
1963 if isinstance(job, FakeBuild):
1964 job.release()
1965 else:
1966 job.waiting = False
1967 self.log.debug("Queued job %s released" % job.unique)
1968 self.gearman_server.wakeConnections()
1969
1970 def getParameter(self, job, name):
1971 if isinstance(job, FakeBuild):
1972 return job.parameters[name]
1973 else:
1974 parameters = json.loads(job.arguments)
1975 return parameters[name]
1976
Clark Boylanb640e052014-04-03 16:41:46 -07001977 def haveAllBuildsReported(self):
1978 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001979 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001980 return False
1981 # Find out if every build that the worker has completed has been
1982 # reported back to Zuul. If it hasn't then that means a Gearman
1983 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001984 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001985 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001986 if not zbuild:
1987 # It has already been reported
1988 continue
1989 # It hasn't been reported yet.
1990 return False
1991 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001992 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001993 if connection.state == 'GRAB_WAIT':
1994 return False
1995 return True
1996
1997 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001998 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001999 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002000 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002001 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002002 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002003 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002004 for j in conn.related_jobs.values():
2005 if j.unique == build.uuid:
2006 client_job = j
2007 break
2008 if not client_job:
2009 self.log.debug("%s is not known to the gearman client" %
2010 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002011 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002012 if not client_job.handle:
2013 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002014 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002015 server_job = self.gearman_server.jobs.get(client_job.handle)
2016 if not server_job:
2017 self.log.debug("%s is not known to the gearman server" %
2018 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002019 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002020 if not hasattr(server_job, 'waiting'):
2021 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002022 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002023 if server_job.waiting:
2024 continue
James E. Blair17302972016-08-10 16:11:42 -07002025 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002026 self.log.debug("%s has not reported start" % build)
2027 return False
Paul Belanger174a8272017-03-14 13:20:10 -04002028 worker_build = self.executor_server.job_builds.get(
2029 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07002030 if worker_build:
2031 if worker_build.isWaiting():
2032 continue
2033 else:
2034 self.log.debug("%s is running" % worker_build)
2035 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002036 else:
James E. Blair962220f2016-08-03 11:22:38 -07002037 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002038 return False
James E. Blaira002b032017-04-18 10:35:48 -07002039 for (build_uuid, job_worker) in \
2040 self.executor_server.job_workers.items():
2041 if build_uuid not in seen_builds:
2042 self.log.debug("%s is not finalized" % build_uuid)
2043 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002044 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002045
James E. Blairdce6cea2016-12-20 16:45:32 -08002046 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002047 if self.fake_nodepool.paused:
2048 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002049 if self.sched.nodepool.requests:
2050 return False
2051 return True
2052
Jan Hruban6b71aff2015-10-22 16:58:08 +02002053 def eventQueuesEmpty(self):
2054 for queue in self.event_queues:
2055 yield queue.empty()
2056
2057 def eventQueuesJoin(self):
2058 for queue in self.event_queues:
2059 queue.join()
2060
Clark Boylanb640e052014-04-03 16:41:46 -07002061 def waitUntilSettled(self):
2062 self.log.debug("Waiting until settled...")
2063 start = time.time()
2064 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002065 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002066 self.log.error("Timeout waiting for Zuul to settle")
2067 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002068 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002069 self.log.error(" %s: %s" % (queue, queue.empty()))
2070 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002071 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002072 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002073 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002074 self.log.error("All requests completed: %s" %
2075 (self.areAllNodeRequestsComplete(),))
2076 self.log.error("Merge client jobs: %s" %
2077 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002078 raise Exception("Timeout waiting for Zuul to settle")
2079 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002080
Paul Belanger174a8272017-03-14 13:20:10 -04002081 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002082 # have all build states propogated to zuul?
2083 if self.haveAllBuildsReported():
2084 # Join ensures that the queue is empty _and_ events have been
2085 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002086 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002087 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002088 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002089 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002090 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002091 self.areAllNodeRequestsComplete() and
2092 all(self.eventQueuesEmpty())):
2093 # The queue empty check is placed at the end to
2094 # ensure that if a component adds an event between
2095 # when locked the run handler and checked that the
2096 # components were stable, we don't erroneously
2097 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002098 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002099 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002100 self.log.debug("...settled.")
2101 return
2102 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002103 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002104 self.sched.wake_event.wait(0.1)
2105
2106 def countJobResults(self, jobs, result):
2107 jobs = filter(lambda x: x.result == result, jobs)
2108 return len(jobs)
2109
James E. Blair96c6bf82016-01-15 16:20:40 -08002110 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002111 for job in self.history:
2112 if (job.name == name and
2113 (project is None or
2114 job.parameters['ZUUL_PROJECT'] == project)):
2115 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002116 raise Exception("Unable to find job %s in history" % name)
2117
2118 def assertEmptyQueues(self):
2119 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002120 for tenant in self.sched.abide.tenants.values():
2121 for pipeline in tenant.layout.pipelines.values():
2122 for queue in pipeline.queues:
2123 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002124 print('pipeline %s queue %s contents %s' % (
2125 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002126 self.assertEqual(len(queue.queue), 0,
2127 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002128
2129 def assertReportedStat(self, key, value=None, kind=None):
2130 start = time.time()
2131 while time.time() < (start + 5):
2132 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002133 k, v = stat.split(':')
2134 if key == k:
2135 if value is None and kind is None:
2136 return
2137 elif value:
2138 if value == v:
2139 return
2140 elif kind:
2141 if v.endswith('|' + kind):
2142 return
2143 time.sleep(0.1)
2144
Clark Boylanb640e052014-04-03 16:41:46 -07002145 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002146
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002147 def assertBuilds(self, builds):
2148 """Assert that the running builds are as described.
2149
2150 The list of running builds is examined and must match exactly
2151 the list of builds described by the input.
2152
2153 :arg list builds: A list of dictionaries. Each item in the
2154 list must match the corresponding build in the build
2155 history, and each element of the dictionary must match the
2156 corresponding attribute of the build.
2157
2158 """
James E. Blair3158e282016-08-19 09:34:11 -07002159 try:
2160 self.assertEqual(len(self.builds), len(builds))
2161 for i, d in enumerate(builds):
2162 for k, v in d.items():
2163 self.assertEqual(
2164 getattr(self.builds[i], k), v,
2165 "Element %i in builds does not match" % (i,))
2166 except Exception:
2167 for build in self.builds:
2168 self.log.error("Running build: %s" % build)
2169 else:
2170 self.log.error("No running builds")
2171 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002172
James E. Blairb536ecc2016-08-31 10:11:42 -07002173 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002174 """Assert that the completed builds are as described.
2175
2176 The list of completed builds is examined and must match
2177 exactly the list of builds described by the input.
2178
2179 :arg list history: A list of dictionaries. Each item in the
2180 list must match the corresponding build in the build
2181 history, and each element of the dictionary must match the
2182 corresponding attribute of the build.
2183
James E. Blairb536ecc2016-08-31 10:11:42 -07002184 :arg bool ordered: If true, the history must match the order
2185 supplied, if false, the builds are permitted to have
2186 arrived in any order.
2187
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002188 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002189 def matches(history_item, item):
2190 for k, v in item.items():
2191 if getattr(history_item, k) != v:
2192 return False
2193 return True
James E. Blair3158e282016-08-19 09:34:11 -07002194 try:
2195 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002196 if ordered:
2197 for i, d in enumerate(history):
2198 if not matches(self.history[i], d):
2199 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002200 "Element %i in history does not match %s" %
2201 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002202 else:
2203 unseen = self.history[:]
2204 for i, d in enumerate(history):
2205 found = False
2206 for unseen_item in unseen:
2207 if matches(unseen_item, d):
2208 found = True
2209 unseen.remove(unseen_item)
2210 break
2211 if not found:
2212 raise Exception("No match found for element %i "
2213 "in history" % (i,))
2214 if unseen:
2215 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002216 except Exception:
2217 for build in self.history:
2218 self.log.error("Completed build: %s" % build)
2219 else:
2220 self.log.error("No completed builds")
2221 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002222
James E. Blair6ac368c2016-12-22 18:07:20 -08002223 def printHistory(self):
2224 """Log the build history.
2225
2226 This can be useful during tests to summarize what jobs have
2227 completed.
2228
2229 """
2230 self.log.debug("Build history:")
2231 for build in self.history:
2232 self.log.debug(build)
2233
James E. Blair59fdbac2015-12-07 17:08:06 -08002234 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002235 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2236
James E. Blair9ea70072017-04-19 16:05:30 -07002237 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002238 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002239 if not os.path.exists(root):
2240 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002241 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2242 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002243- tenant:
2244 name: openstack
2245 source:
2246 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002247 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002248 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002249 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002250 - org/project
2251 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002252 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002253 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002254 self.config.set('zuul', 'tenant_config',
2255 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002256 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002257
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002258 def addCommitToRepo(self, project, message, files,
2259 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002260 path = os.path.join(self.upstream_root, project)
2261 repo = git.Repo(path)
2262 repo.head.reference = branch
2263 zuul.merger.merger.reset_repo_to_head(repo)
2264 for fn, content in files.items():
2265 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002266 try:
2267 os.makedirs(os.path.dirname(fn))
2268 except OSError:
2269 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002270 with open(fn, 'w') as f:
2271 f.write(content)
2272 repo.index.add([fn])
2273 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002274 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002275 repo.heads[branch].commit = commit
2276 repo.head.reference = branch
2277 repo.git.clean('-x', '-f', '-d')
2278 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002279 if tag:
2280 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002281 return before
2282
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002283 def commitConfigUpdate(self, project_name, source_name):
2284 """Commit an update to zuul.yaml
2285
2286 This overwrites the zuul.yaml in the specificed project with
2287 the contents specified.
2288
2289 :arg str project_name: The name of the project containing
2290 zuul.yaml (e.g., common-config)
2291
2292 :arg str source_name: The path to the file (underneath the
2293 test fixture directory) whose contents should be used to
2294 replace zuul.yaml.
2295 """
2296
2297 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002298 files = {}
2299 with open(source_path, 'r') as f:
2300 data = f.read()
2301 layout = yaml.safe_load(data)
2302 files['zuul.yaml'] = data
2303 for item in layout:
2304 if 'job' in item:
2305 jobname = item['job']['name']
2306 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002307 before = self.addCommitToRepo(
2308 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002309 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002310 return before
2311
James E. Blair7fc8daa2016-08-08 15:37:15 -07002312 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002313
James E. Blair7fc8daa2016-08-08 15:37:15 -07002314 """Inject a Fake (Gerrit) event.
2315
2316 This method accepts a JSON-encoded event and simulates Zuul
2317 having received it from Gerrit. It could (and should)
2318 eventually apply to any connection type, but is currently only
2319 used with Gerrit connections. The name of the connection is
2320 used to look up the corresponding server, and the event is
2321 simulated as having been received by all Zuul connections
2322 attached to that server. So if two Gerrit connections in Zuul
2323 are connected to the same Gerrit server, and you invoke this
2324 method specifying the name of one of them, the event will be
2325 received by both.
2326
2327 .. note::
2328
2329 "self.fake_gerrit.addEvent" calls should be migrated to
2330 this method.
2331
2332 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002333 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002334 :arg str event: The JSON-encoded event.
2335
2336 """
2337 specified_conn = self.connections.connections[connection]
2338 for conn in self.connections.connections.values():
2339 if (isinstance(conn, specified_conn.__class__) and
2340 specified_conn.server == conn.server):
2341 conn.addEvent(event)
2342
James E. Blair3f876d52016-07-22 13:07:14 -07002343
2344class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002345 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002346 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002347
Joshua Heskethd78b4482015-09-14 16:56:34 -06002348
2349class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002350 def setup_config(self):
2351 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002352 for section_name in self.config.sections():
2353 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2354 section_name, re.I)
2355 if not con_match:
2356 continue
2357
2358 if self.config.get(section_name, 'driver') == 'sql':
2359 f = MySQLSchemaFixture()
2360 self.useFixture(f)
2361 if (self.config.get(section_name, 'dburi') ==
2362 '$MYSQL_FIXTURE_DBURI$'):
2363 self.config.set(section_name, 'dburi', f.dburi)