blob: 63934466b86ec3932690d381b74ff787c04b89bb [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
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050022import importlib
23from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import statsd
51import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080052import testtools.content
53import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080054from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000055import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070056
James E. Blaire511d2f2016-12-08 15:22:26 -080057import zuul.driver.gerrit.gerritsource as gerritsource
58import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070059import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070060import zuul.scheduler
61import zuul.webapp
62import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020069import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080071import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010072from zuul.exceptions import MergeFailure
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():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
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
Monty Taylorb934c1a2017-06-16 19:31:47 -0500463 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
Clark Boylanb640e052014-04-03 16:41:46 -0700498 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 if 'submit' in action:
501 change.setMerged()
502 if message:
503 change.setReported()
504
505 def query(self, number):
506 change = self.changes.get(int(number))
507 if change:
508 return change.query()
509 return {}
510
James E. Blairc494d542014-08-06 09:23:52 -0700511 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700512 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700513 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800514 if query.startswith('change:'):
515 # Query a specific changeid
516 changeid = query[len('change:'):]
517 l = [change.query() for change in self.changes.values()
518 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700519 elif query.startswith('message:'):
520 # Query the content of a commit message
521 msg = query[len('message:'):].strip()
522 l = [change.query() for change in self.changes.values()
523 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800524 else:
525 # Query all open changes
526 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700527 return l
James E. Blairc494d542014-08-06 09:23:52 -0700528
Joshua Hesketh352264b2015-08-11 23:42:08 +1000529 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700530 pass
531
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200532 def _uploadPack(self, project):
533 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
534 'multi_ack thin-pack side-band side-band-64k ofs-delta '
535 'shallow no-progress include-tag multi_ack_detailed no-done\n')
536 path = os.path.join(self.upstream_root, project.name)
537 repo = git.Repo(path)
538 for ref in repo.refs:
539 r = ref.object.hexsha + ' ' + ref.path + '\n'
540 ret += '%04x%s' % (len(r) + 4, r)
541 ret += '0000'
542 return ret
543
Joshua Hesketh352264b2015-08-11 23:42:08 +1000544 def getGitUrl(self, project):
545 return os.path.join(self.upstream_root, project.name)
546
Clark Boylanb640e052014-04-03 16:41:46 -0700547
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548class GithubChangeReference(git.Reference):
549 _common_path_default = "refs/pull"
550 _points_to_commits_only = True
551
552
553class FakeGithubPullRequest(object):
554
555 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800556 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700557 writers=[], body=''):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700558 """Creates a new PR with several commits.
559 Sends an event about opened PR."""
560 self.github = github
561 self.source = github
562 self.number = number
563 self.project = project
564 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100565 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700566 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100567 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700568 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100569 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700570 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100571 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100572 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800573 self.reviews = []
574 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700575 self.updated_at = None
576 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100577 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100578 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700579 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700580 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100581 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._updateTimeStamp()
583
Jan Hruban570d01c2016-03-10 21:51:32 +0100584 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700585 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100586 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700587 self._updateTimeStamp()
588
Jan Hruban570d01c2016-03-10 21:51:32 +0100589 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700590 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100591 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700592 self._updateTimeStamp()
593
594 def getPullRequestOpenedEvent(self):
595 return self._getPullRequestEvent('opened')
596
597 def getPullRequestSynchronizeEvent(self):
598 return self._getPullRequestEvent('synchronize')
599
600 def getPullRequestReopenedEvent(self):
601 return self._getPullRequestEvent('reopened')
602
603 def getPullRequestClosedEvent(self):
604 return self._getPullRequestEvent('closed')
605
Jesse Keatinga41566f2017-06-14 18:17:51 -0700606 def getPullRequestEditedEvent(self):
607 return self._getPullRequestEvent('edited')
608
Gregory Haynes4fc12542015-04-22 20:38:06 -0700609 def addComment(self, message):
610 self.comments.append(message)
611 self._updateTimeStamp()
612
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200613 def getCommentAddedEvent(self, text):
614 name = 'issue_comment'
615 data = {
616 'action': 'created',
617 'issue': {
618 'number': self.number
619 },
620 'comment': {
621 'body': text
622 },
623 'repository': {
624 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100625 },
626 'sender': {
627 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200628 }
629 }
630 return (name, data)
631
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800632 def getReviewAddedEvent(self, review):
633 name = 'pull_request_review'
634 data = {
635 'action': 'submitted',
636 'pull_request': {
637 'number': self.number,
638 'title': self.subject,
639 'updated_at': self.updated_at,
640 'base': {
641 'ref': self.branch,
642 'repo': {
643 'full_name': self.project
644 }
645 },
646 'head': {
647 'sha': self.head_sha
648 }
649 },
650 'review': {
651 'state': review
652 },
653 'repository': {
654 'full_name': self.project
655 },
656 'sender': {
657 'login': 'ghuser'
658 }
659 }
660 return (name, data)
661
Jan Hruban16ad31f2015-11-07 14:39:07 +0100662 def addLabel(self, name):
663 if name not in self.labels:
664 self.labels.append(name)
665 self._updateTimeStamp()
666 return self._getLabelEvent(name)
667
668 def removeLabel(self, name):
669 if name in self.labels:
670 self.labels.remove(name)
671 self._updateTimeStamp()
672 return self._getUnlabelEvent(name)
673
674 def _getLabelEvent(self, label):
675 name = 'pull_request'
676 data = {
677 'action': 'labeled',
678 'pull_request': {
679 'number': self.number,
680 'updated_at': self.updated_at,
681 'base': {
682 'ref': self.branch,
683 'repo': {
684 'full_name': self.project
685 }
686 },
687 'head': {
688 'sha': self.head_sha
689 }
690 },
691 'label': {
692 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100693 },
694 'sender': {
695 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100696 }
697 }
698 return (name, data)
699
700 def _getUnlabelEvent(self, label):
701 name = 'pull_request'
702 data = {
703 'action': 'unlabeled',
704 'pull_request': {
705 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100706 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100707 'updated_at': self.updated_at,
708 'base': {
709 'ref': self.branch,
710 'repo': {
711 'full_name': self.project
712 }
713 },
714 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800715 'sha': self.head_sha,
716 'repo': {
717 'full_name': self.project
718 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100719 }
720 },
721 'label': {
722 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100723 },
724 'sender': {
725 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100726 }
727 }
728 return (name, data)
729
Jesse Keatinga41566f2017-06-14 18:17:51 -0700730 def editBody(self, body):
731 self.body = body
732 self._updateTimeStamp()
733
Gregory Haynes4fc12542015-04-22 20:38:06 -0700734 def _getRepo(self):
735 repo_path = os.path.join(self.upstream_root, self.project)
736 return git.Repo(repo_path)
737
738 def _createPRRef(self):
739 repo = self._getRepo()
740 GithubChangeReference.create(
741 repo, self._getPRReference(), 'refs/tags/init')
742
Jan Hruban570d01c2016-03-10 21:51:32 +0100743 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700744 repo = self._getRepo()
745 ref = repo.references[self._getPRReference()]
746 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100747 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700748 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100749 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700750 repo.head.reference = ref
751 zuul.merger.merger.reset_repo_to_head(repo)
752 repo.git.clean('-x', '-f', '-d')
753
Jan Hruban570d01c2016-03-10 21:51:32 +0100754 if files:
755 fn = files[0]
756 self.files = files
757 else:
758 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
759 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100760 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700761 fn = os.path.join(repo.working_dir, fn)
762 f = open(fn, 'w')
763 with open(fn, 'w') as f:
764 f.write("test %s %s\n" %
765 (self.branch, self.number))
766 repo.index.add([fn])
767
768 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800769 # Create an empty set of statuses for the given sha,
770 # each sha on a PR may have a status set on it
771 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700772 repo.head.reference = 'master'
773 zuul.merger.merger.reset_repo_to_head(repo)
774 repo.git.clean('-x', '-f', '-d')
775 repo.heads['master'].checkout()
776
777 def _updateTimeStamp(self):
778 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
779
780 def getPRHeadSha(self):
781 repo = self._getRepo()
782 return repo.references[self._getPRReference()].commit.hexsha
783
Jesse Keatingae4cd272017-01-30 17:10:44 -0800784 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800785 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
786 # convert the timestamp to a str format that would be returned
787 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800788
Adam Gandelmand81dd762017-02-09 15:15:49 -0800789 if granted_on:
790 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
791 submitted_at = time.strftime(
792 gh_time_format, granted_on.timetuple())
793 else:
794 # github timestamps only down to the second, so we need to make
795 # sure reviews that tests add appear to be added over a period of
796 # time in the past and not all at once.
797 if not self.reviews:
798 # the first review happens 10 mins ago
799 offset = 600
800 else:
801 # subsequent reviews happen 1 minute closer to now
802 offset = 600 - (len(self.reviews) * 60)
803
804 granted_on = datetime.datetime.utcfromtimestamp(
805 time.time() - offset)
806 submitted_at = time.strftime(
807 gh_time_format, granted_on.timetuple())
808
Jesse Keatingae4cd272017-01-30 17:10:44 -0800809 self.reviews.append({
810 'state': state,
811 'user': {
812 'login': user,
813 'email': user + "@derp.com",
814 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800815 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800816 })
817
Gregory Haynes4fc12542015-04-22 20:38:06 -0700818 def _getPRReference(self):
819 return '%s/head' % self.number
820
821 def _getPullRequestEvent(self, action):
822 name = 'pull_request'
823 data = {
824 'action': action,
825 'number': self.number,
826 'pull_request': {
827 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100828 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700829 'updated_at': self.updated_at,
830 'base': {
831 'ref': self.branch,
832 'repo': {
833 'full_name': self.project
834 }
835 },
836 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800837 'sha': self.head_sha,
838 'repo': {
839 'full_name': self.project
840 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700841 },
842 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100843 },
844 'sender': {
845 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700846 }
847 }
848 return (name, data)
849
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800850 def getCommitStatusEvent(self, context, state='success', user='zuul'):
851 name = 'status'
852 data = {
853 'state': state,
854 'sha': self.head_sha,
855 'description': 'Test results for %s: %s' % (self.head_sha, state),
856 'target_url': 'http://zuul/%s' % self.head_sha,
857 'branches': [],
858 'context': context,
859 'sender': {
860 'login': user
861 }
862 }
863 return (name, data)
864
Gregory Haynes4fc12542015-04-22 20:38:06 -0700865
866class FakeGithubConnection(githubconnection.GithubConnection):
867 log = logging.getLogger("zuul.test.FakeGithubConnection")
868
869 def __init__(self, driver, connection_name, connection_config,
870 upstream_root=None):
871 super(FakeGithubConnection, self).__init__(driver, connection_name,
872 connection_config)
873 self.connection_name = connection_name
874 self.pr_number = 0
875 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700876 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100878 self.merge_failure = False
879 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100880 self.reports = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700881
Jesse Keatinga41566f2017-06-14 18:17:51 -0700882 def openFakePullRequest(self, project, branch, subject, files=[],
883 body=''):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700884 self.pr_number += 1
885 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100886 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700887 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700888 self.pull_requests.append(pull_request)
889 return pull_request
890
Jesse Keating71a47ff2017-06-06 11:36:43 -0700891 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
892 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700893 if not old_rev:
894 old_rev = '00000000000000000000000000000000'
895 if not new_rev:
896 new_rev = random_sha1()
897 name = 'push'
898 data = {
899 'ref': ref,
900 'before': old_rev,
901 'after': new_rev,
902 'repository': {
903 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700904 },
905 'commits': [
906 {
907 'added': added_files,
908 'removed': removed_files,
909 'modified': modified_files
910 }
911 ]
Wayne1a78c612015-06-11 17:14:13 -0700912 }
913 return (name, data)
914
Gregory Haynes4fc12542015-04-22 20:38:06 -0700915 def emitEvent(self, event):
916 """Emulates sending the GitHub webhook event to the connection."""
917 port = self.webapp.server.socket.getsockname()[1]
918 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700919 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700920 headers = {'X-Github-Event': name}
921 req = urllib.request.Request(
922 'http://localhost:%s/connection/%s/payload'
923 % (port, self.connection_name),
924 data=payload, headers=headers)
925 urllib.request.urlopen(req)
926
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200927 def getPull(self, project, number):
928 pr = self.pull_requests[number - 1]
929 data = {
930 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100931 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200932 'updated_at': pr.updated_at,
933 'base': {
934 'repo': {
935 'full_name': pr.project
936 },
937 'ref': pr.branch,
938 },
Jan Hruban37615e52015-11-19 14:30:49 +0100939 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700940 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200941 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800942 'sha': pr.head_sha,
943 'repo': {
944 'full_name': pr.project
945 }
Jesse Keating61040e72017-06-08 15:08:27 -0700946 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700947 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700948 'labels': pr.labels,
949 'merged': pr.is_merged,
950 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200951 }
952 return data
953
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800954 def getPullBySha(self, sha):
955 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
956 if len(prs) > 1:
957 raise Exception('Multiple pulls found with head sha: %s' % sha)
958 pr = prs[0]
959 return self.getPull(pr.project, pr.number)
960
Jesse Keatingae4cd272017-01-30 17:10:44 -0800961 def _getPullReviews(self, owner, project, number):
962 pr = self.pull_requests[number - 1]
963 return pr.reviews
964
Jan Hruban3b415922016-02-03 13:10:22 +0100965 def getUser(self, login):
966 data = {
967 'username': login,
968 'name': 'Github User',
969 'email': 'github.user@example.com'
970 }
971 return data
972
Jesse Keatingae4cd272017-01-30 17:10:44 -0800973 def getRepoPermission(self, project, login):
974 owner, proj = project.split('/')
975 for pr in self.pull_requests:
976 pr_owner, pr_project = pr.project.split('/')
977 if (pr_owner == owner and proj == pr_project):
978 if login in pr.writers:
979 return 'write'
980 else:
981 return 'read'
982
Gregory Haynes4fc12542015-04-22 20:38:06 -0700983 def getGitUrl(self, project):
984 return os.path.join(self.upstream_root, str(project))
985
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200986 def real_getGitUrl(self, project):
987 return super(FakeGithubConnection, self).getGitUrl(project)
988
Gregory Haynes4fc12542015-04-22 20:38:06 -0700989 def getProjectBranches(self, project):
990 """Masks getProjectBranches since we don't have a real github"""
991
992 # just returns master for now
993 return ['master']
994
Jan Hrubane252a732017-01-03 15:03:09 +0100995 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +0100996 # record that this got reported
997 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -0700998 pull_request = self.pull_requests[pr_number - 1]
999 pull_request.addComment(message)
1000
Jan Hruban3b415922016-02-03 13:10:22 +01001001 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001002 # record that this got reported
1003 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001004 pull_request = self.pull_requests[pr_number - 1]
1005 if self.merge_failure:
1006 raise Exception('Pull request was not merged')
1007 if self.merge_not_allowed_count > 0:
1008 self.merge_not_allowed_count -= 1
1009 raise MergeFailure('Merge was not successful due to mergeability'
1010 ' conflict')
1011 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001012 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001013
Jesse Keatingd96e5882017-01-19 13:55:50 -08001014 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001015 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001016
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001017 def setCommitStatus(self, project, sha, state, url='', description='',
1018 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001019 # record that this got reported
1020 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001021 # always insert a status to the front of the list, to represent
1022 # the last status provided for a commit.
1023 # Since we're bypassing github API, which would require a user, we
1024 # default the user as 'zuul' here.
1025 self.statuses.setdefault(project, {}).setdefault(sha, [])
1026 self.statuses[project][sha].insert(0, {
1027 'state': state,
1028 'url': url,
1029 'description': description,
1030 'context': context,
1031 'creator': {
1032 'login': user
1033 }
1034 })
Jan Hrubane252a732017-01-03 15:03:09 +01001035
Jan Hruban16ad31f2015-11-07 14:39:07 +01001036 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001037 # record that this got reported
1038 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001039 pull_request = self.pull_requests[pr_number - 1]
1040 pull_request.addLabel(label)
1041
1042 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001043 # record that this got reported
1044 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001045 pull_request = self.pull_requests[pr_number - 1]
1046 pull_request.removeLabel(label)
1047
Jesse Keatinga41566f2017-06-14 18:17:51 -07001048 def _getNeededByFromPR(self, change):
1049 prs = []
1050 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
1051 (self.git_host, change.project.name,
1052 change.number))
1053 for pr in self.pull_requests:
1054 if pattern.search(pr.body):
1055 # Get our version of a pull so that it's a dict
1056 pull = self.getPull(pr.project, pr.number)
1057 prs.append(pull)
1058
1059 return prs
1060
Gregory Haynes4fc12542015-04-22 20:38:06 -07001061
Clark Boylanb640e052014-04-03 16:41:46 -07001062class BuildHistory(object):
1063 def __init__(self, **kw):
1064 self.__dict__.update(kw)
1065
1066 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001067 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1068 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001069
1070
Clark Boylanb640e052014-04-03 16:41:46 -07001071class FakeStatsd(threading.Thread):
1072 def __init__(self):
1073 threading.Thread.__init__(self)
1074 self.daemon = True
1075 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1076 self.sock.bind(('', 0))
1077 self.port = self.sock.getsockname()[1]
1078 self.wake_read, self.wake_write = os.pipe()
1079 self.stats = []
1080
1081 def run(self):
1082 while True:
1083 poll = select.poll()
1084 poll.register(self.sock, select.POLLIN)
1085 poll.register(self.wake_read, select.POLLIN)
1086 ret = poll.poll()
1087 for (fd, event) in ret:
1088 if fd == self.sock.fileno():
1089 data = self.sock.recvfrom(1024)
1090 if not data:
1091 return
1092 self.stats.append(data[0])
1093 if fd == self.wake_read:
1094 return
1095
1096 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001097 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001098
1099
James E. Blaire1767bc2016-08-02 10:00:27 -07001100class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001101 log = logging.getLogger("zuul.test")
1102
Paul Belanger174a8272017-03-14 13:20:10 -04001103 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001104 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001105 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001106 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001107 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001108 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001109 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001110 # TODOv3(jeblair): self.node is really "the label of the node
1111 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001112 # keep using it like this, or we may end up exposing more of
1113 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001114 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001115 self.node = None
1116 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001117 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001118 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001119 self.pipeline = self.parameters['ZUUL_PIPELINE']
1120 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001121 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.wait_condition = threading.Condition()
1123 self.waiting = False
1124 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001125 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001126 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001127 self.changes = None
1128 if 'ZUUL_CHANGE_IDS' in self.parameters:
1129 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001130
James E. Blair3158e282016-08-19 09:34:11 -07001131 def __repr__(self):
1132 waiting = ''
1133 if self.waiting:
1134 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001135 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1136 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001137
Clark Boylanb640e052014-04-03 16:41:46 -07001138 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001139 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001140 self.wait_condition.acquire()
1141 self.wait_condition.notify()
1142 self.waiting = False
1143 self.log.debug("Build %s released" % self.unique)
1144 self.wait_condition.release()
1145
1146 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001147 """Return whether this build is being held.
1148
1149 :returns: Whether the build is being held.
1150 :rtype: bool
1151 """
1152
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.wait_condition.acquire()
1154 if self.waiting:
1155 ret = True
1156 else:
1157 ret = False
1158 self.wait_condition.release()
1159 return ret
1160
1161 def _wait(self):
1162 self.wait_condition.acquire()
1163 self.waiting = True
1164 self.log.debug("Build %s waiting" % self.unique)
1165 self.wait_condition.wait()
1166 self.wait_condition.release()
1167
1168 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001169 self.log.debug('Running build %s' % self.unique)
1170
Paul Belanger174a8272017-03-14 13:20:10 -04001171 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001172 self.log.debug('Holding build %s' % self.unique)
1173 self._wait()
1174 self.log.debug("Build %s continuing" % self.unique)
1175
James E. Blair412fba82017-01-26 15:00:50 -08001176 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001177 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001178 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001179 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001180 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001181 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001182 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001183
James E. Blaire1767bc2016-08-02 10:00:27 -07001184 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001185
James E. Blaira5dba232016-08-08 15:53:24 -07001186 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001187 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001188 for change in changes:
1189 if self.hasChanges(change):
1190 return True
1191 return False
1192
James E. Blaire7b99a02016-08-05 14:27:34 -07001193 def hasChanges(self, *changes):
1194 """Return whether this build has certain changes in its git repos.
1195
1196 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001197 are expected to be present (in order) in the git repository of
1198 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001199
1200 :returns: Whether the build has the indicated changes.
1201 :rtype: bool
1202
1203 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001204 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001205 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001206 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001207 try:
1208 repo = git.Repo(path)
1209 except NoSuchPathError as e:
1210 self.log.debug('%s' % e)
1211 return False
1212 ref = self.parameters['ZUUL_REF']
1213 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1214 commit_message = '%s-1' % change.subject
1215 self.log.debug("Checking if build %s has changes; commit_message "
1216 "%s; repo_messages %s" % (self, commit_message,
1217 repo_messages))
1218 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001219 self.log.debug(" messages do not match")
1220 return False
1221 self.log.debug(" OK")
1222 return True
1223
James E. Blaird8af5422017-05-24 13:59:40 -07001224 def getWorkspaceRepos(self, projects):
1225 """Return workspace git repo objects for the listed projects
1226
1227 :arg list projects: A list of strings, each the canonical name
1228 of a project.
1229
1230 :returns: A dictionary of {name: repo} for every listed
1231 project.
1232 :rtype: dict
1233
1234 """
1235
1236 repos = {}
1237 for project in projects:
1238 path = os.path.join(self.jobdir.src_root, project)
1239 repo = git.Repo(path)
1240 repos[project] = repo
1241 return repos
1242
Clark Boylanb640e052014-04-03 16:41:46 -07001243
Paul Belanger174a8272017-03-14 13:20:10 -04001244class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1245 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001246
Paul Belanger174a8272017-03-14 13:20:10 -04001247 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001248 they will report that they have started but then pause until
1249 released before reporting completion. This attribute may be
1250 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001251 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001252 be explicitly released.
1253
1254 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001255 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001257 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001258 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001259 self.hold_jobs_in_build = False
1260 self.lock = threading.Lock()
1261 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001262 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001263 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001264 self.job_builds = {}
Monty Taylorde8242c2017-02-23 20:29:53 -06001265 self.hostname = 'zl.example.com'
James E. Blairf5dbd002015-12-23 15:26:17 -08001266
James E. Blaira5dba232016-08-08 15:53:24 -07001267 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001268 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001269
1270 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001271 :arg Change change: The :py:class:`~tests.base.FakeChange`
1272 instance which should cause the job to fail. This job
1273 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001274
1275 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001276 l = self.fail_tests.get(name, [])
1277 l.append(change)
1278 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001279
James E. Blair962220f2016-08-03 11:22:38 -07001280 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001281 """Release a held build.
1282
1283 :arg str regex: A regular expression which, if supplied, will
1284 cause only builds with matching names to be released. If
1285 not supplied, all builds will be released.
1286
1287 """
James E. Blair962220f2016-08-03 11:22:38 -07001288 builds = self.running_builds[:]
1289 self.log.debug("Releasing build %s (%s)" % (regex,
1290 len(self.running_builds)))
1291 for build in builds:
1292 if not regex or re.match(regex, build.name):
1293 self.log.debug("Releasing build %s" %
1294 (build.parameters['ZUUL_UUID']))
1295 build.release()
1296 else:
1297 self.log.debug("Not releasing build %s" %
1298 (build.parameters['ZUUL_UUID']))
1299 self.log.debug("Done releasing builds %s (%s)" %
1300 (regex, len(self.running_builds)))
1301
Paul Belanger174a8272017-03-14 13:20:10 -04001302 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001303 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001304 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001305 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001306 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001307 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001308 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001309 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001310 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1311 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001312
1313 def stopJob(self, job):
1314 self.log.debug("handle stop")
1315 parameters = json.loads(job.arguments)
1316 uuid = parameters['uuid']
1317 for build in self.running_builds:
1318 if build.unique == uuid:
1319 build.aborted = True
1320 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001321 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001322
James E. Blaira002b032017-04-18 10:35:48 -07001323 def stop(self):
1324 for build in self.running_builds:
1325 build.release()
1326 super(RecordingExecutorServer, self).stop()
1327
Joshua Hesketh50c21782016-10-13 21:34:14 +11001328
Paul Belanger174a8272017-03-14 13:20:10 -04001329class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001330 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001331 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001332 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001333 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001334 if not commit: # merge conflict
1335 self.recordResult('MERGER_FAILURE')
1336 return commit
1337
1338 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001339 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001340 self.executor_server.lock.acquire()
1341 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001342 BuildHistory(name=build.name, result=result, changes=build.changes,
1343 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001344 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001345 pipeline=build.parameters['ZUUL_PIPELINE'])
1346 )
Paul Belanger174a8272017-03-14 13:20:10 -04001347 self.executor_server.running_builds.remove(build)
1348 del self.executor_server.job_builds[self.job.unique]
1349 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001350
1351 def runPlaybooks(self, args):
1352 build = self.executor_server.job_builds[self.job.unique]
1353 build.jobdir = self.jobdir
1354
1355 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1356 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001357 return result
1358
Monty Taylore6562aa2017-02-20 07:37:39 -05001359 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001360 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001361
Paul Belanger174a8272017-03-14 13:20:10 -04001362 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001363 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001364 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001365 else:
1366 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001367 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001368
James E. Blairad8dca02017-02-21 11:48:32 -05001369 def getHostList(self, args):
1370 self.log.debug("hostlist")
1371 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001372 for host in hosts:
1373 host['host_vars']['ansible_connection'] = 'local'
1374
1375 hosts.append(dict(
1376 name='localhost',
1377 host_vars=dict(ansible_connection='local'),
1378 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001379 return hosts
1380
James E. Blairf5dbd002015-12-23 15:26:17 -08001381
Clark Boylanb640e052014-04-03 16:41:46 -07001382class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001383 """A Gearman server for use in tests.
1384
1385 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1386 added to the queue but will not be distributed to workers
1387 until released. This attribute may be changed at any time and
1388 will take effect for subsequently enqueued jobs, but
1389 previously held jobs will still need to be explicitly
1390 released.
1391
1392 """
1393
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001394 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001395 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001396 if use_ssl:
1397 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1398 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1399 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1400 else:
1401 ssl_ca = None
1402 ssl_cert = None
1403 ssl_key = None
1404
1405 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1406 ssl_cert=ssl_cert,
1407 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001408
1409 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001410 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1411 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001412 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001413 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001414 job.waiting = self.hold_jobs_in_queue
1415 else:
1416 job.waiting = False
1417 if job.waiting:
1418 continue
1419 if job.name in connection.functions:
1420 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001421 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001422 connection.related_jobs[job.handle] = job
1423 job.worker_connection = connection
1424 job.running = True
1425 return job
1426 return None
1427
1428 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001429 """Release a held job.
1430
1431 :arg str regex: A regular expression which, if supplied, will
1432 cause only jobs with matching names to be released. If
1433 not supplied, all jobs will be released.
1434 """
Clark Boylanb640e052014-04-03 16:41:46 -07001435 released = False
1436 qlen = (len(self.high_queue) + len(self.normal_queue) +
1437 len(self.low_queue))
1438 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1439 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001440 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001441 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001442 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001443 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001444 self.log.debug("releasing queued job %s" %
1445 job.unique)
1446 job.waiting = False
1447 released = True
1448 else:
1449 self.log.debug("not releasing queued job %s" %
1450 job.unique)
1451 if released:
1452 self.wakeConnections()
1453 qlen = (len(self.high_queue) + len(self.normal_queue) +
1454 len(self.low_queue))
1455 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1456
1457
1458class FakeSMTP(object):
1459 log = logging.getLogger('zuul.FakeSMTP')
1460
1461 def __init__(self, messages, server, port):
1462 self.server = server
1463 self.port = port
1464 self.messages = messages
1465
1466 def sendmail(self, from_email, to_email, msg):
1467 self.log.info("Sending email from %s, to %s, with msg %s" % (
1468 from_email, to_email, msg))
1469
1470 headers = msg.split('\n\n', 1)[0]
1471 body = msg.split('\n\n', 1)[1]
1472
1473 self.messages.append(dict(
1474 from_email=from_email,
1475 to_email=to_email,
1476 msg=msg,
1477 headers=headers,
1478 body=body,
1479 ))
1480
1481 return True
1482
1483 def quit(self):
1484 return True
1485
1486
James E. Blairdce6cea2016-12-20 16:45:32 -08001487class FakeNodepool(object):
1488 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001489 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001490
1491 log = logging.getLogger("zuul.test.FakeNodepool")
1492
1493 def __init__(self, host, port, chroot):
1494 self.client = kazoo.client.KazooClient(
1495 hosts='%s:%s%s' % (host, port, chroot))
1496 self.client.start()
1497 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001498 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001499 self.thread = threading.Thread(target=self.run)
1500 self.thread.daemon = True
1501 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001502 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001503
1504 def stop(self):
1505 self._running = False
1506 self.thread.join()
1507 self.client.stop()
1508 self.client.close()
1509
1510 def run(self):
1511 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001512 try:
1513 self._run()
1514 except Exception:
1515 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001516 time.sleep(0.1)
1517
1518 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001519 if self.paused:
1520 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001521 for req in self.getNodeRequests():
1522 self.fulfillRequest(req)
1523
1524 def getNodeRequests(self):
1525 try:
1526 reqids = self.client.get_children(self.REQUEST_ROOT)
1527 except kazoo.exceptions.NoNodeError:
1528 return []
1529 reqs = []
1530 for oid in sorted(reqids):
1531 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001532 try:
1533 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001534 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001535 data['_oid'] = oid
1536 reqs.append(data)
1537 except kazoo.exceptions.NoNodeError:
1538 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001539 return reqs
1540
James E. Blaire18d4602017-01-05 11:17:28 -08001541 def getNodes(self):
1542 try:
1543 nodeids = self.client.get_children(self.NODE_ROOT)
1544 except kazoo.exceptions.NoNodeError:
1545 return []
1546 nodes = []
1547 for oid in sorted(nodeids):
1548 path = self.NODE_ROOT + '/' + oid
1549 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001550 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001551 data['_oid'] = oid
1552 try:
1553 lockfiles = self.client.get_children(path + '/lock')
1554 except kazoo.exceptions.NoNodeError:
1555 lockfiles = []
1556 if lockfiles:
1557 data['_lock'] = True
1558 else:
1559 data['_lock'] = False
1560 nodes.append(data)
1561 return nodes
1562
James E. Blaira38c28e2017-01-04 10:33:20 -08001563 def makeNode(self, request_id, node_type):
1564 now = time.time()
1565 path = '/nodepool/nodes/'
1566 data = dict(type=node_type,
1567 provider='test-provider',
1568 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001569 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001570 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001571 public_ipv4='127.0.0.1',
1572 private_ipv4=None,
1573 public_ipv6=None,
1574 allocated_to=request_id,
1575 state='ready',
1576 state_time=now,
1577 created_time=now,
1578 updated_time=now,
1579 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001580 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001581 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001582 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001583 path = self.client.create(path, data,
1584 makepath=True,
1585 sequence=True)
1586 nodeid = path.split("/")[-1]
1587 return nodeid
1588
James E. Blair6ab79e02017-01-06 10:10:17 -08001589 def addFailRequest(self, request):
1590 self.fail_requests.add(request['_oid'])
1591
James E. Blairdce6cea2016-12-20 16:45:32 -08001592 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001593 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001594 return
1595 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001596 oid = request['_oid']
1597 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001598
James E. Blair6ab79e02017-01-06 10:10:17 -08001599 if oid in self.fail_requests:
1600 request['state'] = 'failed'
1601 else:
1602 request['state'] = 'fulfilled'
1603 nodes = []
1604 for node in request['node_types']:
1605 nodeid = self.makeNode(oid, node)
1606 nodes.append(nodeid)
1607 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001608
James E. Blaira38c28e2017-01-04 10:33:20 -08001609 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001610 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001611 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001612 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001613 try:
1614 self.client.set(path, data)
1615 except kazoo.exceptions.NoNodeError:
1616 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001617
1618
James E. Blair498059b2016-12-20 13:50:13 -08001619class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001620 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001621 super(ChrootedKazooFixture, self).__init__()
1622
1623 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1624 if ':' in zk_host:
1625 host, port = zk_host.split(':')
1626 else:
1627 host = zk_host
1628 port = None
1629
1630 self.zookeeper_host = host
1631
1632 if not port:
1633 self.zookeeper_port = 2181
1634 else:
1635 self.zookeeper_port = int(port)
1636
Clark Boylan621ec9a2017-04-07 17:41:33 -07001637 self.test_id = test_id
1638
James E. Blair498059b2016-12-20 13:50:13 -08001639 def _setUp(self):
1640 # Make sure the test chroot paths do not conflict
1641 random_bits = ''.join(random.choice(string.ascii_lowercase +
1642 string.ascii_uppercase)
1643 for x in range(8))
1644
Clark Boylan621ec9a2017-04-07 17:41:33 -07001645 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001646 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1647
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001648 self.addCleanup(self._cleanup)
1649
James E. Blair498059b2016-12-20 13:50:13 -08001650 # Ensure the chroot path exists and clean up any pre-existing znodes.
1651 _tmp_client = kazoo.client.KazooClient(
1652 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1653 _tmp_client.start()
1654
1655 if _tmp_client.exists(self.zookeeper_chroot):
1656 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1657
1658 _tmp_client.ensure_path(self.zookeeper_chroot)
1659 _tmp_client.stop()
1660 _tmp_client.close()
1661
James E. Blair498059b2016-12-20 13:50:13 -08001662 def _cleanup(self):
1663 '''Remove the chroot path.'''
1664 # Need a non-chroot'ed client to remove the chroot path
1665 _tmp_client = kazoo.client.KazooClient(
1666 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1667 _tmp_client.start()
1668 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1669 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001670 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001671
1672
Joshua Heskethd78b4482015-09-14 16:56:34 -06001673class MySQLSchemaFixture(fixtures.Fixture):
1674 def setUp(self):
1675 super(MySQLSchemaFixture, self).setUp()
1676
1677 random_bits = ''.join(random.choice(string.ascii_lowercase +
1678 string.ascii_uppercase)
1679 for x in range(8))
1680 self.name = '%s_%s' % (random_bits, os.getpid())
1681 self.passwd = uuid.uuid4().hex
1682 db = pymysql.connect(host="localhost",
1683 user="openstack_citest",
1684 passwd="openstack_citest",
1685 db="openstack_citest")
1686 cur = db.cursor()
1687 cur.execute("create database %s" % self.name)
1688 cur.execute(
1689 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1690 (self.name, self.name, self.passwd))
1691 cur.execute("flush privileges")
1692
1693 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1694 self.passwd,
1695 self.name)
1696 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1697 self.addCleanup(self.cleanup)
1698
1699 def cleanup(self):
1700 db = pymysql.connect(host="localhost",
1701 user="openstack_citest",
1702 passwd="openstack_citest",
1703 db="openstack_citest")
1704 cur = db.cursor()
1705 cur.execute("drop database %s" % self.name)
1706 cur.execute("drop user '%s'@'localhost'" % self.name)
1707 cur.execute("flush privileges")
1708
1709
Maru Newby3fe5f852015-01-13 04:22:14 +00001710class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001711 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001712 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001713
James E. Blair1c236df2017-02-01 14:07:24 -08001714 def attachLogs(self, *args):
1715 def reader():
1716 self._log_stream.seek(0)
1717 while True:
1718 x = self._log_stream.read(4096)
1719 if not x:
1720 break
1721 yield x.encode('utf8')
1722 content = testtools.content.content_from_reader(
1723 reader,
1724 testtools.content_type.UTF8_TEXT,
1725 False)
1726 self.addDetail('logging', content)
1727
Clark Boylanb640e052014-04-03 16:41:46 -07001728 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001729 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001730 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1731 try:
1732 test_timeout = int(test_timeout)
1733 except ValueError:
1734 # If timeout value is invalid do not set a timeout.
1735 test_timeout = 0
1736 if test_timeout > 0:
1737 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1738
1739 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1740 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1741 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1742 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1743 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1744 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1745 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1746 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1747 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1748 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001749 self._log_stream = StringIO()
1750 self.addOnException(self.attachLogs)
1751 else:
1752 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001753
James E. Blair73b41772017-05-22 13:22:55 -07001754 # NOTE(jeblair): this is temporary extra debugging to try to
1755 # track down a possible leak.
1756 orig_git_repo_init = git.Repo.__init__
1757
1758 def git_repo_init(myself, *args, **kw):
1759 orig_git_repo_init(myself, *args, **kw)
1760 self.log.debug("Created git repo 0x%x %s" %
1761 (id(myself), repr(myself)))
1762
1763 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1764 git_repo_init))
1765
James E. Blair1c236df2017-02-01 14:07:24 -08001766 handler = logging.StreamHandler(self._log_stream)
1767 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1768 '%(levelname)-8s %(message)s')
1769 handler.setFormatter(formatter)
1770
1771 logger = logging.getLogger()
1772 logger.setLevel(logging.DEBUG)
1773 logger.addHandler(handler)
1774
Clark Boylan3410d532017-04-25 12:35:29 -07001775 # Make sure we don't carry old handlers around in process state
1776 # which slows down test runs
1777 self.addCleanup(logger.removeHandler, handler)
1778 self.addCleanup(handler.close)
1779 self.addCleanup(handler.flush)
1780
James E. Blair1c236df2017-02-01 14:07:24 -08001781 # NOTE(notmorgan): Extract logging overrides for specific
1782 # libraries from the OS_LOG_DEFAULTS env and create loggers
1783 # for each. This is used to limit the output during test runs
1784 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001785 log_defaults_from_env = os.environ.get(
1786 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001787 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001788
James E. Blairdce6cea2016-12-20 16:45:32 -08001789 if log_defaults_from_env:
1790 for default in log_defaults_from_env.split(','):
1791 try:
1792 name, level_str = default.split('=', 1)
1793 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001794 logger = logging.getLogger(name)
1795 logger.setLevel(level)
1796 logger.addHandler(handler)
1797 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001798 except ValueError:
1799 # NOTE(notmorgan): Invalid format of the log default,
1800 # skip and don't try and apply a logger for the
1801 # specified module
1802 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001803
Maru Newby3fe5f852015-01-13 04:22:14 +00001804
1805class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001806 """A test case with a functioning Zuul.
1807
1808 The following class variables are used during test setup and can
1809 be overidden by subclasses but are effectively read-only once a
1810 test method starts running:
1811
1812 :cvar str config_file: This points to the main zuul config file
1813 within the fixtures directory. Subclasses may override this
1814 to obtain a different behavior.
1815
1816 :cvar str tenant_config_file: This is the tenant config file
1817 (which specifies from what git repos the configuration should
1818 be loaded). It defaults to the value specified in
1819 `config_file` but can be overidden by subclasses to obtain a
1820 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001821 configuration. See also the :py:func:`simple_layout`
1822 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001823
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001824 :cvar bool create_project_keys: Indicates whether Zuul should
1825 auto-generate keys for each project, or whether the test
1826 infrastructure should insert dummy keys to save time during
1827 startup. Defaults to False.
1828
James E. Blaire7b99a02016-08-05 14:27:34 -07001829 The following are instance variables that are useful within test
1830 methods:
1831
1832 :ivar FakeGerritConnection fake_<connection>:
1833 A :py:class:`~tests.base.FakeGerritConnection` will be
1834 instantiated for each connection present in the config file
1835 and stored here. For instance, `fake_gerrit` will hold the
1836 FakeGerritConnection object for a connection named `gerrit`.
1837
1838 :ivar FakeGearmanServer gearman_server: An instance of
1839 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1840 server that all of the Zuul components in this test use to
1841 communicate with each other.
1842
Paul Belanger174a8272017-03-14 13:20:10 -04001843 :ivar RecordingExecutorServer executor_server: An instance of
1844 :py:class:`~tests.base.RecordingExecutorServer` which is the
1845 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001846
1847 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1848 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001849 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001850 list upon completion.
1851
1852 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1853 objects representing completed builds. They are appended to
1854 the list in the order they complete.
1855
1856 """
1857
James E. Blair83005782015-12-11 14:46:03 -08001858 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001859 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001860 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001861 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001862
1863 def _startMerger(self):
1864 self.merge_server = zuul.merger.server.MergeServer(self.config,
1865 self.connections)
1866 self.merge_server.start()
1867
Maru Newby3fe5f852015-01-13 04:22:14 +00001868 def setUp(self):
1869 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001870
1871 self.setupZK()
1872
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001873 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001874 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001875 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1876 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001877 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001878 tmp_root = tempfile.mkdtemp(
1879 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001880 self.test_root = os.path.join(tmp_root, "zuul-test")
1881 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001882 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001883 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001884 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001885
1886 if os.path.exists(self.test_root):
1887 shutil.rmtree(self.test_root)
1888 os.makedirs(self.test_root)
1889 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001890 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001891
1892 # Make per test copy of Configuration.
1893 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001894 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1895 if not os.path.exists(self.private_key_file):
1896 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1897 shutil.copy(src_private_key_file, self.private_key_file)
1898 shutil.copy('{}.pub'.format(src_private_key_file),
1899 '{}.pub'.format(self.private_key_file))
1900 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001901 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001902 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001903 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001904 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001905 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001906 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001907 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001908
Clark Boylanb640e052014-04-03 16:41:46 -07001909 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001910 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1911 # see: https://github.com/jsocol/pystatsd/issues/61
1912 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001913 os.environ['STATSD_PORT'] = str(self.statsd.port)
1914 self.statsd.start()
1915 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001916 importlib.reload(statsd)
1917 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001918
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001919 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001920
1921 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001922 self.log.info("Gearman server on port %s" %
1923 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001924 if self.use_ssl:
1925 self.log.info('SSL enabled for gearman')
1926 self.config.set(
1927 'gearman', 'ssl_ca',
1928 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1929 self.config.set(
1930 'gearman', 'ssl_cert',
1931 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1932 self.config.set(
1933 'gearman', 'ssl_key',
1934 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001935
James E. Blaire511d2f2016-12-08 15:22:26 -08001936 gerritsource.GerritSource.replication_timeout = 1.5
1937 gerritsource.GerritSource.replication_retry_interval = 0.5
1938 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001939
Joshua Hesketh352264b2015-08-11 23:42:08 +10001940 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001941
Jan Hruban7083edd2015-08-21 14:00:54 +02001942 self.webapp = zuul.webapp.WebApp(
1943 self.sched, port=0, listen_address='127.0.0.1')
1944
Jan Hruban6b71aff2015-10-22 16:58:08 +02001945 self.event_queues = [
1946 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001947 self.sched.trigger_event_queue,
1948 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001949 ]
1950
James E. Blairfef78942016-03-11 16:28:56 -08001951 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001952 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001953
Paul Belanger174a8272017-03-14 13:20:10 -04001954 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001955 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001956 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001957 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001958 _test_root=self.test_root,
1959 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001960 self.executor_server.start()
1961 self.history = self.executor_server.build_history
1962 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001963
Paul Belanger174a8272017-03-14 13:20:10 -04001964 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001965 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001966 self.merge_client = zuul.merger.client.MergeClient(
1967 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001968 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001969 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001970 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001971
James E. Blair0d5a36e2017-02-21 10:53:44 -05001972 self.fake_nodepool = FakeNodepool(
1973 self.zk_chroot_fixture.zookeeper_host,
1974 self.zk_chroot_fixture.zookeeper_port,
1975 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001976
Paul Belanger174a8272017-03-14 13:20:10 -04001977 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001978 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001979 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001980 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001981
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001982 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001983
1984 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001985 self.webapp.start()
1986 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001987 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001988 # Cleanups are run in reverse order
1989 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001990 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001991 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001992
James E. Blairb9c0d772017-03-03 14:34:49 -08001993 self.sched.reconfigure(self.config)
1994 self.sched.resume()
1995
Tobias Henkel7df274b2017-05-26 17:41:11 +02001996 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001997 # Set up gerrit related fakes
1998 # Set a changes database so multiple FakeGerrit's can report back to
1999 # a virtual canonical database given by the configured hostname
2000 self.gerrit_changes_dbs = {}
2001
2002 def getGerritConnection(driver, name, config):
2003 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2004 con = FakeGerritConnection(driver, name, config,
2005 changes_db=db,
2006 upstream_root=self.upstream_root)
2007 self.event_queues.append(con.event_queue)
2008 setattr(self, 'fake_' + name, con)
2009 return con
2010
2011 self.useFixture(fixtures.MonkeyPatch(
2012 'zuul.driver.gerrit.GerritDriver.getConnection',
2013 getGerritConnection))
2014
Gregory Haynes4fc12542015-04-22 20:38:06 -07002015 def getGithubConnection(driver, name, config):
2016 con = FakeGithubConnection(driver, name, config,
2017 upstream_root=self.upstream_root)
2018 setattr(self, 'fake_' + name, con)
2019 return con
2020
2021 self.useFixture(fixtures.MonkeyPatch(
2022 'zuul.driver.github.GithubDriver.getConnection',
2023 getGithubConnection))
2024
James E. Blaire511d2f2016-12-08 15:22:26 -08002025 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002026 # TODO(jhesketh): This should come from lib.connections for better
2027 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002028 # Register connections from the config
2029 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002030
Joshua Hesketh352264b2015-08-11 23:42:08 +10002031 def FakeSMTPFactory(*args, **kw):
2032 args = [self.smtp_messages] + list(args)
2033 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002034
Joshua Hesketh352264b2015-08-11 23:42:08 +10002035 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002036
James E. Blaire511d2f2016-12-08 15:22:26 -08002037 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002038 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002039 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002040
James E. Blair83005782015-12-11 14:46:03 -08002041 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002042 # This creates the per-test configuration object. It can be
2043 # overriden by subclasses, but should not need to be since it
2044 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002045 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002046 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002047
2048 if not self.setupSimpleLayout():
2049 if hasattr(self, 'tenant_config_file'):
2050 self.config.set('zuul', 'tenant_config',
2051 self.tenant_config_file)
2052 git_path = os.path.join(
2053 os.path.dirname(
2054 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2055 'git')
2056 if os.path.exists(git_path):
2057 for reponame in os.listdir(git_path):
2058 project = reponame.replace('_', '/')
2059 self.copyDirToRepo(project,
2060 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002061 # Make test_root persist after ansible run for .flag test
2062 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002063 self.setupAllProjectKeys()
2064
James E. Blair06cc3922017-04-19 10:08:10 -07002065 def setupSimpleLayout(self):
2066 # If the test method has been decorated with a simple_layout,
2067 # use that instead of the class tenant_config_file. Set up a
2068 # single config-project with the specified layout, and
2069 # initialize repos for all of the 'project' entries which
2070 # appear in the layout.
2071 test_name = self.id().split('.')[-1]
2072 test = getattr(self, test_name)
2073 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002074 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002075 else:
2076 return False
2077
James E. Blairb70e55a2017-04-19 12:57:02 -07002078 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002079 path = os.path.join(FIXTURE_DIR, path)
2080 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002081 data = f.read()
2082 layout = yaml.safe_load(data)
2083 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002084 untrusted_projects = []
2085 for item in layout:
2086 if 'project' in item:
2087 name = item['project']['name']
2088 untrusted_projects.append(name)
2089 self.init_repo(name)
2090 self.addCommitToRepo(name, 'initial commit',
2091 files={'README': ''},
2092 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002093 if 'job' in item:
2094 jobname = item['job']['name']
2095 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002096
2097 root = os.path.join(self.test_root, "config")
2098 if not os.path.exists(root):
2099 os.makedirs(root)
2100 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2101 config = [{'tenant':
2102 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002103 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002104 {'config-projects': ['common-config'],
2105 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002106 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002107 f.close()
2108 self.config.set('zuul', 'tenant_config',
2109 os.path.join(FIXTURE_DIR, f.name))
2110
2111 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002112 self.addCommitToRepo('common-config', 'add content from fixture',
2113 files, branch='master', tag='init')
2114
2115 return True
2116
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002117 def setupAllProjectKeys(self):
2118 if self.create_project_keys:
2119 return
2120
2121 path = self.config.get('zuul', 'tenant_config')
2122 with open(os.path.join(FIXTURE_DIR, path)) as f:
2123 tenant_config = yaml.safe_load(f.read())
2124 for tenant in tenant_config:
2125 sources = tenant['tenant']['source']
2126 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002127 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002128 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002129 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002130 self.setupProjectKeys(source, project)
2131
2132 def setupProjectKeys(self, source, project):
2133 # Make sure we set up an RSA key for the project so that we
2134 # don't spend time generating one:
2135
2136 key_root = os.path.join(self.state_root, 'keys')
2137 if not os.path.isdir(key_root):
2138 os.mkdir(key_root, 0o700)
2139 private_key_file = os.path.join(key_root, source, project + '.pem')
2140 private_key_dir = os.path.dirname(private_key_file)
2141 self.log.debug("Installing test keys for project %s at %s" % (
2142 project, private_key_file))
2143 if not os.path.isdir(private_key_dir):
2144 os.makedirs(private_key_dir)
2145 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2146 with open(private_key_file, 'w') as o:
2147 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002148
James E. Blair498059b2016-12-20 13:50:13 -08002149 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002150 self.zk_chroot_fixture = self.useFixture(
2151 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002152 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002153 self.zk_chroot_fixture.zookeeper_host,
2154 self.zk_chroot_fixture.zookeeper_port,
2155 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002156
James E. Blair96c6bf82016-01-15 16:20:40 -08002157 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002158 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002159
2160 files = {}
2161 for (dirpath, dirnames, filenames) in os.walk(source_path):
2162 for filename in filenames:
2163 test_tree_filepath = os.path.join(dirpath, filename)
2164 common_path = os.path.commonprefix([test_tree_filepath,
2165 source_path])
2166 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2167 with open(test_tree_filepath, 'r') as f:
2168 content = f.read()
2169 files[relative_filepath] = content
2170 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002171 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002172
James E. Blaire18d4602017-01-05 11:17:28 -08002173 def assertNodepoolState(self):
2174 # Make sure that there are no pending requests
2175
2176 requests = self.fake_nodepool.getNodeRequests()
2177 self.assertEqual(len(requests), 0)
2178
2179 nodes = self.fake_nodepool.getNodes()
2180 for node in nodes:
2181 self.assertFalse(node['_lock'], "Node %s is locked" %
2182 (node['_oid'],))
2183
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002184 def assertNoGeneratedKeys(self):
2185 # Make sure that Zuul did not generate any project keys
2186 # (unless it was supposed to).
2187
2188 if self.create_project_keys:
2189 return
2190
2191 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2192 test_key = i.read()
2193
2194 key_root = os.path.join(self.state_root, 'keys')
2195 for root, dirname, files in os.walk(key_root):
2196 for fn in files:
2197 with open(os.path.join(root, fn)) as f:
2198 self.assertEqual(test_key, f.read())
2199
Clark Boylanb640e052014-04-03 16:41:46 -07002200 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002201 self.log.debug("Assert final state")
2202 # Make sure no jobs are running
2203 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002204 # Make sure that git.Repo objects have been garbage collected.
2205 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002206 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002207 gc.collect()
2208 for obj in gc.get_objects():
2209 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002210 self.log.debug("Leaked git repo object: 0x%x %s" %
2211 (id(obj), repr(obj)))
2212 for ref in gc.get_referrers(obj):
2213 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002214 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002215 if repos:
2216 for obj in gc.garbage:
2217 self.log.debug(" Garbage %s" % (repr(obj)))
2218 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002219 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002220 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002221 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002222 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002223 for tenant in self.sched.abide.tenants.values():
2224 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002225 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002226 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002227
2228 def shutdown(self):
2229 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002230 self.executor_server.hold_jobs_in_build = False
2231 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002232 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002233 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002234 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002235 self.sched.stop()
2236 self.sched.join()
2237 self.statsd.stop()
2238 self.statsd.join()
2239 self.webapp.stop()
2240 self.webapp.join()
2241 self.rpc.stop()
2242 self.rpc.join()
2243 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002244 self.fake_nodepool.stop()
2245 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002246 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002247 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002248 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002249 # Further the pydevd threads also need to be whitelisted so debugging
2250 # e.g. in PyCharm is possible without breaking shutdown.
2251 whitelist = ['executor-watchdog',
2252 'pydevd.CommandThread',
2253 'pydevd.Reader',
2254 'pydevd.Writer',
2255 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002256 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002257 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002258 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002259 log_str = ""
2260 for thread_id, stack_frame in sys._current_frames().items():
2261 log_str += "Thread: %s\n" % thread_id
2262 log_str += "".join(traceback.format_stack(stack_frame))
2263 self.log.debug(log_str)
2264 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002265
James E. Blaira002b032017-04-18 10:35:48 -07002266 def assertCleanShutdown(self):
2267 pass
2268
James E. Blairc4ba97a2017-04-19 16:26:24 -07002269 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002270 parts = project.split('/')
2271 path = os.path.join(self.upstream_root, *parts[:-1])
2272 if not os.path.exists(path):
2273 os.makedirs(path)
2274 path = os.path.join(self.upstream_root, project)
2275 repo = git.Repo.init(path)
2276
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002277 with repo.config_writer() as config_writer:
2278 config_writer.set_value('user', 'email', 'user@example.com')
2279 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002280
Clark Boylanb640e052014-04-03 16:41:46 -07002281 repo.index.commit('initial commit')
2282 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002283 if tag:
2284 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002285
James E. Blair97d902e2014-08-21 13:25:56 -07002286 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002287 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002288 repo.git.clean('-x', '-f', '-d')
2289
James E. Blair97d902e2014-08-21 13:25:56 -07002290 def create_branch(self, project, branch):
2291 path = os.path.join(self.upstream_root, project)
2292 repo = git.Repo.init(path)
2293 fn = os.path.join(path, 'README')
2294
2295 branch_head = repo.create_head(branch)
2296 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002297 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002298 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002299 f.close()
2300 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002301 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002302
James E. Blair97d902e2014-08-21 13:25:56 -07002303 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002304 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002305 repo.git.clean('-x', '-f', '-d')
2306
Sachi King9f16d522016-03-16 12:20:45 +11002307 def create_commit(self, project):
2308 path = os.path.join(self.upstream_root, project)
2309 repo = git.Repo(path)
2310 repo.head.reference = repo.heads['master']
2311 file_name = os.path.join(path, 'README')
2312 with open(file_name, 'a') as f:
2313 f.write('creating fake commit\n')
2314 repo.index.add([file_name])
2315 commit = repo.index.commit('Creating a fake commit')
2316 return commit.hexsha
2317
James E. Blairf4a5f022017-04-18 14:01:10 -07002318 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002319 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002320 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002321 while len(self.builds):
2322 self.release(self.builds[0])
2323 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002324 i += 1
2325 if count is not None and i >= count:
2326 break
James E. Blairb8c16472015-05-05 14:55:26 -07002327
Clark Boylanb640e052014-04-03 16:41:46 -07002328 def release(self, job):
2329 if isinstance(job, FakeBuild):
2330 job.release()
2331 else:
2332 job.waiting = False
2333 self.log.debug("Queued job %s released" % job.unique)
2334 self.gearman_server.wakeConnections()
2335
2336 def getParameter(self, job, name):
2337 if isinstance(job, FakeBuild):
2338 return job.parameters[name]
2339 else:
2340 parameters = json.loads(job.arguments)
2341 return parameters[name]
2342
Clark Boylanb640e052014-04-03 16:41:46 -07002343 def haveAllBuildsReported(self):
2344 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002345 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002346 return False
2347 # Find out if every build that the worker has completed has been
2348 # reported back to Zuul. If it hasn't then that means a Gearman
2349 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002350 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002351 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002352 if not zbuild:
2353 # It has already been reported
2354 continue
2355 # It hasn't been reported yet.
2356 return False
2357 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002358 worker = self.executor_server.executor_worker
2359 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002360 if connection.state == 'GRAB_WAIT':
2361 return False
2362 return True
2363
2364 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002365 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002366 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002367 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002368 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002369 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002370 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002371 for j in conn.related_jobs.values():
2372 if j.unique == build.uuid:
2373 client_job = j
2374 break
2375 if not client_job:
2376 self.log.debug("%s is not known to the gearman client" %
2377 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002378 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002379 if not client_job.handle:
2380 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002381 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002382 server_job = self.gearman_server.jobs.get(client_job.handle)
2383 if not server_job:
2384 self.log.debug("%s is not known to the gearman server" %
2385 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002386 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002387 if not hasattr(server_job, 'waiting'):
2388 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002389 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002390 if server_job.waiting:
2391 continue
James E. Blair17302972016-08-10 16:11:42 -07002392 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002393 self.log.debug("%s has not reported start" % build)
2394 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002395 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002396 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002397 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002398 if worker_build:
2399 if worker_build.isWaiting():
2400 continue
2401 else:
2402 self.log.debug("%s is running" % worker_build)
2403 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002404 else:
James E. Blair962220f2016-08-03 11:22:38 -07002405 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002406 return False
James E. Blaira002b032017-04-18 10:35:48 -07002407 for (build_uuid, job_worker) in \
2408 self.executor_server.job_workers.items():
2409 if build_uuid not in seen_builds:
2410 self.log.debug("%s is not finalized" % build_uuid)
2411 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002412 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002413
James E. Blairdce6cea2016-12-20 16:45:32 -08002414 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002415 if self.fake_nodepool.paused:
2416 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002417 if self.sched.nodepool.requests:
2418 return False
2419 return True
2420
Jan Hruban6b71aff2015-10-22 16:58:08 +02002421 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002422 for event_queue in self.event_queues:
2423 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002424
2425 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002426 for event_queue in self.event_queues:
2427 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002428
Clark Boylanb640e052014-04-03 16:41:46 -07002429 def waitUntilSettled(self):
2430 self.log.debug("Waiting until settled...")
2431 start = time.time()
2432 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002433 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002434 self.log.error("Timeout waiting for Zuul to settle")
2435 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002436 for event_queue in self.event_queues:
2437 self.log.error(" %s: %s" %
2438 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002439 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002440 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002441 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002442 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002443 self.log.error("All requests completed: %s" %
2444 (self.areAllNodeRequestsComplete(),))
2445 self.log.error("Merge client jobs: %s" %
2446 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002447 raise Exception("Timeout waiting for Zuul to settle")
2448 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002449
Paul Belanger174a8272017-03-14 13:20:10 -04002450 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002451 # have all build states propogated to zuul?
2452 if self.haveAllBuildsReported():
2453 # Join ensures that the queue is empty _and_ events have been
2454 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002455 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002456 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002457 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002458 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002459 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002460 self.areAllNodeRequestsComplete() and
2461 all(self.eventQueuesEmpty())):
2462 # The queue empty check is placed at the end to
2463 # ensure that if a component adds an event between
2464 # when locked the run handler and checked that the
2465 # components were stable, we don't erroneously
2466 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002467 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002468 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002469 self.log.debug("...settled.")
2470 return
2471 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002472 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002473 self.sched.wake_event.wait(0.1)
2474
2475 def countJobResults(self, jobs, result):
2476 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002477 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002478
Monty Taylor0d926122017-05-24 08:07:56 -05002479 def getBuildByName(self, name):
2480 for build in self.builds:
2481 if build.name == name:
2482 return build
2483 raise Exception("Unable to find build %s" % name)
2484
James E. Blair96c6bf82016-01-15 16:20:40 -08002485 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002486 for job in self.history:
2487 if (job.name == name and
2488 (project is None or
2489 job.parameters['ZUUL_PROJECT'] == project)):
2490 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002491 raise Exception("Unable to find job %s in history" % name)
2492
2493 def assertEmptyQueues(self):
2494 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002495 for tenant in self.sched.abide.tenants.values():
2496 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002497 for pipeline_queue in pipeline.queues:
2498 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002499 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002500 pipeline.name, pipeline_queue.name,
2501 pipeline_queue.queue))
2502 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002503 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002504
2505 def assertReportedStat(self, key, value=None, kind=None):
2506 start = time.time()
2507 while time.time() < (start + 5):
2508 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002509 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002510 if key == k:
2511 if value is None and kind is None:
2512 return
2513 elif value:
2514 if value == v:
2515 return
2516 elif kind:
2517 if v.endswith('|' + kind):
2518 return
2519 time.sleep(0.1)
2520
Clark Boylanb640e052014-04-03 16:41:46 -07002521 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002522
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002523 def assertBuilds(self, builds):
2524 """Assert that the running builds are as described.
2525
2526 The list of running builds is examined and must match exactly
2527 the list of builds described by the input.
2528
2529 :arg list builds: A list of dictionaries. Each item in the
2530 list must match the corresponding build in the build
2531 history, and each element of the dictionary must match the
2532 corresponding attribute of the build.
2533
2534 """
James E. Blair3158e282016-08-19 09:34:11 -07002535 try:
2536 self.assertEqual(len(self.builds), len(builds))
2537 for i, d in enumerate(builds):
2538 for k, v in d.items():
2539 self.assertEqual(
2540 getattr(self.builds[i], k), v,
2541 "Element %i in builds does not match" % (i,))
2542 except Exception:
2543 for build in self.builds:
2544 self.log.error("Running build: %s" % build)
2545 else:
2546 self.log.error("No running builds")
2547 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002548
James E. Blairb536ecc2016-08-31 10:11:42 -07002549 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002550 """Assert that the completed builds are as described.
2551
2552 The list of completed builds is examined and must match
2553 exactly the list of builds described by the input.
2554
2555 :arg list history: A list of dictionaries. Each item in the
2556 list must match the corresponding build in the build
2557 history, and each element of the dictionary must match the
2558 corresponding attribute of the build.
2559
James E. Blairb536ecc2016-08-31 10:11:42 -07002560 :arg bool ordered: If true, the history must match the order
2561 supplied, if false, the builds are permitted to have
2562 arrived in any order.
2563
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002564 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002565 def matches(history_item, item):
2566 for k, v in item.items():
2567 if getattr(history_item, k) != v:
2568 return False
2569 return True
James E. Blair3158e282016-08-19 09:34:11 -07002570 try:
2571 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002572 if ordered:
2573 for i, d in enumerate(history):
2574 if not matches(self.history[i], d):
2575 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002576 "Element %i in history does not match %s" %
2577 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002578 else:
2579 unseen = self.history[:]
2580 for i, d in enumerate(history):
2581 found = False
2582 for unseen_item in unseen:
2583 if matches(unseen_item, d):
2584 found = True
2585 unseen.remove(unseen_item)
2586 break
2587 if not found:
2588 raise Exception("No match found for element %i "
2589 "in history" % (i,))
2590 if unseen:
2591 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002592 except Exception:
2593 for build in self.history:
2594 self.log.error("Completed build: %s" % build)
2595 else:
2596 self.log.error("No completed builds")
2597 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002598
James E. Blair6ac368c2016-12-22 18:07:20 -08002599 def printHistory(self):
2600 """Log the build history.
2601
2602 This can be useful during tests to summarize what jobs have
2603 completed.
2604
2605 """
2606 self.log.debug("Build history:")
2607 for build in self.history:
2608 self.log.debug(build)
2609
James E. Blair59fdbac2015-12-07 17:08:06 -08002610 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002611 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2612
James E. Blair9ea70072017-04-19 16:05:30 -07002613 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002614 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002615 if not os.path.exists(root):
2616 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002617 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2618 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002619- tenant:
2620 name: openstack
2621 source:
2622 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002623 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002624 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002625 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002626 - org/project
2627 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002628 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002629 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002630 self.config.set('zuul', 'tenant_config',
2631 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002632 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002633
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002634 def addCommitToRepo(self, project, message, files,
2635 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002636 path = os.path.join(self.upstream_root, project)
2637 repo = git.Repo(path)
2638 repo.head.reference = branch
2639 zuul.merger.merger.reset_repo_to_head(repo)
2640 for fn, content in files.items():
2641 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002642 try:
2643 os.makedirs(os.path.dirname(fn))
2644 except OSError:
2645 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002646 with open(fn, 'w') as f:
2647 f.write(content)
2648 repo.index.add([fn])
2649 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002650 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002651 repo.heads[branch].commit = commit
2652 repo.head.reference = branch
2653 repo.git.clean('-x', '-f', '-d')
2654 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002655 if tag:
2656 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002657 return before
2658
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002659 def commitConfigUpdate(self, project_name, source_name):
2660 """Commit an update to zuul.yaml
2661
2662 This overwrites the zuul.yaml in the specificed project with
2663 the contents specified.
2664
2665 :arg str project_name: The name of the project containing
2666 zuul.yaml (e.g., common-config)
2667
2668 :arg str source_name: The path to the file (underneath the
2669 test fixture directory) whose contents should be used to
2670 replace zuul.yaml.
2671 """
2672
2673 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002674 files = {}
2675 with open(source_path, 'r') as f:
2676 data = f.read()
2677 layout = yaml.safe_load(data)
2678 files['zuul.yaml'] = data
2679 for item in layout:
2680 if 'job' in item:
2681 jobname = item['job']['name']
2682 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002683 before = self.addCommitToRepo(
2684 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002685 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002686 return before
2687
James E. Blair7fc8daa2016-08-08 15:37:15 -07002688 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002689
James E. Blair7fc8daa2016-08-08 15:37:15 -07002690 """Inject a Fake (Gerrit) event.
2691
2692 This method accepts a JSON-encoded event and simulates Zuul
2693 having received it from Gerrit. It could (and should)
2694 eventually apply to any connection type, but is currently only
2695 used with Gerrit connections. The name of the connection is
2696 used to look up the corresponding server, and the event is
2697 simulated as having been received by all Zuul connections
2698 attached to that server. So if two Gerrit connections in Zuul
2699 are connected to the same Gerrit server, and you invoke this
2700 method specifying the name of one of them, the event will be
2701 received by both.
2702
2703 .. note::
2704
2705 "self.fake_gerrit.addEvent" calls should be migrated to
2706 this method.
2707
2708 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002709 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002710 :arg str event: The JSON-encoded event.
2711
2712 """
2713 specified_conn = self.connections.connections[connection]
2714 for conn in self.connections.connections.values():
2715 if (isinstance(conn, specified_conn.__class__) and
2716 specified_conn.server == conn.server):
2717 conn.addEvent(event)
2718
James E. Blaird8af5422017-05-24 13:59:40 -07002719 def getUpstreamRepos(self, projects):
2720 """Return upstream git repo objects for the listed projects
2721
2722 :arg list projects: A list of strings, each the canonical name
2723 of a project.
2724
2725 :returns: A dictionary of {name: repo} for every listed
2726 project.
2727 :rtype: dict
2728
2729 """
2730
2731 repos = {}
2732 for project in projects:
2733 # FIXME(jeblair): the upstream root does not yet have a
2734 # hostname component; that needs to be added, and this
2735 # line removed:
2736 tmp_project_name = '/'.join(project.split('/')[1:])
2737 path = os.path.join(self.upstream_root, tmp_project_name)
2738 repo = git.Repo(path)
2739 repos[project] = repo
2740 return repos
2741
James E. Blair3f876d52016-07-22 13:07:14 -07002742
2743class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002744 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002745 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002746
Joshua Heskethd78b4482015-09-14 16:56:34 -06002747
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002748class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002749 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002750 use_ssl = True
2751
2752
Joshua Heskethd78b4482015-09-14 16:56:34 -06002753class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002754 def setup_config(self):
2755 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002756 for section_name in self.config.sections():
2757 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2758 section_name, re.I)
2759 if not con_match:
2760 continue
2761
2762 if self.config.get(section_name, 'driver') == 'sql':
2763 f = MySQLSchemaFixture()
2764 self.useFixture(f)
2765 if (self.config.get(section_name, 'dburi') ==
2766 '$MYSQL_FIXTURE_DBURI$'):
2767 self.config.set(section_name, 'dburi', f.dburi)