blob: e605c87baac68e8246ba68cd42a012fd3b3c3f6e [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. Blair39840362017-06-23 20:34:02 +01001901 self.config.set('scheduler', 'tenant_config',
1902 os.path.join(
1903 FIXTURE_DIR,
1904 self.config.get('scheduler', 'tenant_config')))
1905 self.config.set('zuul', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001906 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001907 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001908 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001909
Clark Boylanb640e052014-04-03 16:41:46 -07001910 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001911 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1912 # see: https://github.com/jsocol/pystatsd/issues/61
1913 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001914 os.environ['STATSD_PORT'] = str(self.statsd.port)
1915 self.statsd.start()
1916 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001917 importlib.reload(statsd)
1918 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001919
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001920 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001921
1922 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001923 self.log.info("Gearman server on port %s" %
1924 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001925 if self.use_ssl:
1926 self.log.info('SSL enabled for gearman')
1927 self.config.set(
1928 'gearman', 'ssl_ca',
1929 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1930 self.config.set(
1931 'gearman', 'ssl_cert',
1932 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1933 self.config.set(
1934 'gearman', 'ssl_key',
1935 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001936
James E. Blaire511d2f2016-12-08 15:22:26 -08001937 gerritsource.GerritSource.replication_timeout = 1.5
1938 gerritsource.GerritSource.replication_retry_interval = 0.5
1939 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001940
Joshua Hesketh352264b2015-08-11 23:42:08 +10001941 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001942
Jan Hruban7083edd2015-08-21 14:00:54 +02001943 self.webapp = zuul.webapp.WebApp(
1944 self.sched, port=0, listen_address='127.0.0.1')
1945
Jan Hruban6b71aff2015-10-22 16:58:08 +02001946 self.event_queues = [
1947 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001948 self.sched.trigger_event_queue,
1949 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001950 ]
1951
James E. Blairfef78942016-03-11 16:28:56 -08001952 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001953 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001954
Paul Belanger174a8272017-03-14 13:20:10 -04001955 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001956 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001957 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001958 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001959 _test_root=self.test_root,
1960 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001961 self.executor_server.start()
1962 self.history = self.executor_server.build_history
1963 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001964
Paul Belanger174a8272017-03-14 13:20:10 -04001965 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001966 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001967 self.merge_client = zuul.merger.client.MergeClient(
1968 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001969 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001970 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001971 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001972
James E. Blair0d5a36e2017-02-21 10:53:44 -05001973 self.fake_nodepool = FakeNodepool(
1974 self.zk_chroot_fixture.zookeeper_host,
1975 self.zk_chroot_fixture.zookeeper_port,
1976 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001977
Paul Belanger174a8272017-03-14 13:20:10 -04001978 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001979 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001980 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001981 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001982
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001983 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001984
1985 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001986 self.webapp.start()
1987 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001988 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001989 # Cleanups are run in reverse order
1990 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001991 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001992 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001993
James E. Blairb9c0d772017-03-03 14:34:49 -08001994 self.sched.reconfigure(self.config)
1995 self.sched.resume()
1996
Tobias Henkel7df274b2017-05-26 17:41:11 +02001997 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001998 # Set up gerrit related fakes
1999 # Set a changes database so multiple FakeGerrit's can report back to
2000 # a virtual canonical database given by the configured hostname
2001 self.gerrit_changes_dbs = {}
2002
2003 def getGerritConnection(driver, name, config):
2004 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2005 con = FakeGerritConnection(driver, name, config,
2006 changes_db=db,
2007 upstream_root=self.upstream_root)
2008 self.event_queues.append(con.event_queue)
2009 setattr(self, 'fake_' + name, con)
2010 return con
2011
2012 self.useFixture(fixtures.MonkeyPatch(
2013 'zuul.driver.gerrit.GerritDriver.getConnection',
2014 getGerritConnection))
2015
Gregory Haynes4fc12542015-04-22 20:38:06 -07002016 def getGithubConnection(driver, name, config):
2017 con = FakeGithubConnection(driver, name, config,
2018 upstream_root=self.upstream_root)
2019 setattr(self, 'fake_' + name, con)
2020 return con
2021
2022 self.useFixture(fixtures.MonkeyPatch(
2023 'zuul.driver.github.GithubDriver.getConnection',
2024 getGithubConnection))
2025
James E. Blaire511d2f2016-12-08 15:22:26 -08002026 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002027 # TODO(jhesketh): This should come from lib.connections for better
2028 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002029 # Register connections from the config
2030 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002031
Joshua Hesketh352264b2015-08-11 23:42:08 +10002032 def FakeSMTPFactory(*args, **kw):
2033 args = [self.smtp_messages] + list(args)
2034 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002035
Joshua Hesketh352264b2015-08-11 23:42:08 +10002036 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002037
James E. Blaire511d2f2016-12-08 15:22:26 -08002038 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002039 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002040 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002041
James E. Blair83005782015-12-11 14:46:03 -08002042 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002043 # This creates the per-test configuration object. It can be
2044 # overriden by subclasses, but should not need to be since it
2045 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002046 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002047 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002048
James E. Blair39840362017-06-23 20:34:02 +01002049 sections = ['zuul', 'scheduler', 'executor', 'merger']
2050 for section in sections:
2051 if not self.config.has_section(section):
2052 self.config.add_section(section)
2053
James E. Blair06cc3922017-04-19 10:08:10 -07002054 if not self.setupSimpleLayout():
2055 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002056 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002057 self.tenant_config_file)
2058 git_path = os.path.join(
2059 os.path.dirname(
2060 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2061 'git')
2062 if os.path.exists(git_path):
2063 for reponame in os.listdir(git_path):
2064 project = reponame.replace('_', '/')
2065 self.copyDirToRepo(project,
2066 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002067 # Make test_root persist after ansible run for .flag test
2068 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002069 self.setupAllProjectKeys()
2070
James E. Blair06cc3922017-04-19 10:08:10 -07002071 def setupSimpleLayout(self):
2072 # If the test method has been decorated with a simple_layout,
2073 # use that instead of the class tenant_config_file. Set up a
2074 # single config-project with the specified layout, and
2075 # initialize repos for all of the 'project' entries which
2076 # appear in the layout.
2077 test_name = self.id().split('.')[-1]
2078 test = getattr(self, test_name)
2079 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002080 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002081 else:
2082 return False
2083
James E. Blairb70e55a2017-04-19 12:57:02 -07002084 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002085 path = os.path.join(FIXTURE_DIR, path)
2086 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002087 data = f.read()
2088 layout = yaml.safe_load(data)
2089 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002090 untrusted_projects = []
2091 for item in layout:
2092 if 'project' in item:
2093 name = item['project']['name']
2094 untrusted_projects.append(name)
2095 self.init_repo(name)
2096 self.addCommitToRepo(name, 'initial commit',
2097 files={'README': ''},
2098 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002099 if 'job' in item:
2100 jobname = item['job']['name']
2101 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002102
2103 root = os.path.join(self.test_root, "config")
2104 if not os.path.exists(root):
2105 os.makedirs(root)
2106 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2107 config = [{'tenant':
2108 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002109 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002110 {'config-projects': ['common-config'],
2111 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002112 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002113 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002114 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002115 os.path.join(FIXTURE_DIR, f.name))
2116
2117 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002118 self.addCommitToRepo('common-config', 'add content from fixture',
2119 files, branch='master', tag='init')
2120
2121 return True
2122
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002123 def setupAllProjectKeys(self):
2124 if self.create_project_keys:
2125 return
2126
James E. Blair39840362017-06-23 20:34:02 +01002127 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002128 with open(os.path.join(FIXTURE_DIR, path)) as f:
2129 tenant_config = yaml.safe_load(f.read())
2130 for tenant in tenant_config:
2131 sources = tenant['tenant']['source']
2132 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002133 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002134 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002135 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002136 self.setupProjectKeys(source, project)
2137
2138 def setupProjectKeys(self, source, project):
2139 # Make sure we set up an RSA key for the project so that we
2140 # don't spend time generating one:
2141
2142 key_root = os.path.join(self.state_root, 'keys')
2143 if not os.path.isdir(key_root):
2144 os.mkdir(key_root, 0o700)
2145 private_key_file = os.path.join(key_root, source, project + '.pem')
2146 private_key_dir = os.path.dirname(private_key_file)
2147 self.log.debug("Installing test keys for project %s at %s" % (
2148 project, private_key_file))
2149 if not os.path.isdir(private_key_dir):
2150 os.makedirs(private_key_dir)
2151 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2152 with open(private_key_file, 'w') as o:
2153 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002154
James E. Blair498059b2016-12-20 13:50:13 -08002155 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002156 self.zk_chroot_fixture = self.useFixture(
2157 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002158 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002159 self.zk_chroot_fixture.zookeeper_host,
2160 self.zk_chroot_fixture.zookeeper_port,
2161 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002162
James E. Blair96c6bf82016-01-15 16:20:40 -08002163 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002164 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002165
2166 files = {}
2167 for (dirpath, dirnames, filenames) in os.walk(source_path):
2168 for filename in filenames:
2169 test_tree_filepath = os.path.join(dirpath, filename)
2170 common_path = os.path.commonprefix([test_tree_filepath,
2171 source_path])
2172 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2173 with open(test_tree_filepath, 'r') as f:
2174 content = f.read()
2175 files[relative_filepath] = content
2176 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002177 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002178
James E. Blaire18d4602017-01-05 11:17:28 -08002179 def assertNodepoolState(self):
2180 # Make sure that there are no pending requests
2181
2182 requests = self.fake_nodepool.getNodeRequests()
2183 self.assertEqual(len(requests), 0)
2184
2185 nodes = self.fake_nodepool.getNodes()
2186 for node in nodes:
2187 self.assertFalse(node['_lock'], "Node %s is locked" %
2188 (node['_oid'],))
2189
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002190 def assertNoGeneratedKeys(self):
2191 # Make sure that Zuul did not generate any project keys
2192 # (unless it was supposed to).
2193
2194 if self.create_project_keys:
2195 return
2196
2197 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2198 test_key = i.read()
2199
2200 key_root = os.path.join(self.state_root, 'keys')
2201 for root, dirname, files in os.walk(key_root):
2202 for fn in files:
2203 with open(os.path.join(root, fn)) as f:
2204 self.assertEqual(test_key, f.read())
2205
Clark Boylanb640e052014-04-03 16:41:46 -07002206 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002207 self.log.debug("Assert final state")
2208 # Make sure no jobs are running
2209 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002210 # Make sure that git.Repo objects have been garbage collected.
2211 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002212 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002213 gc.collect()
2214 for obj in gc.get_objects():
2215 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002216 self.log.debug("Leaked git repo object: 0x%x %s" %
2217 (id(obj), repr(obj)))
2218 for ref in gc.get_referrers(obj):
2219 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002220 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002221 if repos:
2222 for obj in gc.garbage:
2223 self.log.debug(" Garbage %s" % (repr(obj)))
2224 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002225 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002226 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002227 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002228 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002229 for tenant in self.sched.abide.tenants.values():
2230 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002231 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002232 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002233
2234 def shutdown(self):
2235 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002236 self.executor_server.hold_jobs_in_build = False
2237 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002238 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002239 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002240 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002241 self.sched.stop()
2242 self.sched.join()
2243 self.statsd.stop()
2244 self.statsd.join()
2245 self.webapp.stop()
2246 self.webapp.join()
2247 self.rpc.stop()
2248 self.rpc.join()
2249 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002250 self.fake_nodepool.stop()
2251 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002252 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002253 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002254 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002255 # Further the pydevd threads also need to be whitelisted so debugging
2256 # e.g. in PyCharm is possible without breaking shutdown.
2257 whitelist = ['executor-watchdog',
2258 'pydevd.CommandThread',
2259 'pydevd.Reader',
2260 'pydevd.Writer',
2261 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002262 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002263 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002264 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002265 log_str = ""
2266 for thread_id, stack_frame in sys._current_frames().items():
2267 log_str += "Thread: %s\n" % thread_id
2268 log_str += "".join(traceback.format_stack(stack_frame))
2269 self.log.debug(log_str)
2270 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002271
James E. Blaira002b032017-04-18 10:35:48 -07002272 def assertCleanShutdown(self):
2273 pass
2274
James E. Blairc4ba97a2017-04-19 16:26:24 -07002275 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002276 parts = project.split('/')
2277 path = os.path.join(self.upstream_root, *parts[:-1])
2278 if not os.path.exists(path):
2279 os.makedirs(path)
2280 path = os.path.join(self.upstream_root, project)
2281 repo = git.Repo.init(path)
2282
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002283 with repo.config_writer() as config_writer:
2284 config_writer.set_value('user', 'email', 'user@example.com')
2285 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002286
Clark Boylanb640e052014-04-03 16:41:46 -07002287 repo.index.commit('initial commit')
2288 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002289 if tag:
2290 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002291
James E. Blair97d902e2014-08-21 13:25:56 -07002292 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002293 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002294 repo.git.clean('-x', '-f', '-d')
2295
James E. Blair97d902e2014-08-21 13:25:56 -07002296 def create_branch(self, project, branch):
2297 path = os.path.join(self.upstream_root, project)
2298 repo = git.Repo.init(path)
2299 fn = os.path.join(path, 'README')
2300
2301 branch_head = repo.create_head(branch)
2302 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002303 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002304 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002305 f.close()
2306 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002307 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002308
James E. Blair97d902e2014-08-21 13:25:56 -07002309 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002310 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002311 repo.git.clean('-x', '-f', '-d')
2312
Sachi King9f16d522016-03-16 12:20:45 +11002313 def create_commit(self, project):
2314 path = os.path.join(self.upstream_root, project)
2315 repo = git.Repo(path)
2316 repo.head.reference = repo.heads['master']
2317 file_name = os.path.join(path, 'README')
2318 with open(file_name, 'a') as f:
2319 f.write('creating fake commit\n')
2320 repo.index.add([file_name])
2321 commit = repo.index.commit('Creating a fake commit')
2322 return commit.hexsha
2323
James E. Blairf4a5f022017-04-18 14:01:10 -07002324 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002325 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002326 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002327 while len(self.builds):
2328 self.release(self.builds[0])
2329 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002330 i += 1
2331 if count is not None and i >= count:
2332 break
James E. Blairb8c16472015-05-05 14:55:26 -07002333
Clark Boylanb640e052014-04-03 16:41:46 -07002334 def release(self, job):
2335 if isinstance(job, FakeBuild):
2336 job.release()
2337 else:
2338 job.waiting = False
2339 self.log.debug("Queued job %s released" % job.unique)
2340 self.gearman_server.wakeConnections()
2341
2342 def getParameter(self, job, name):
2343 if isinstance(job, FakeBuild):
2344 return job.parameters[name]
2345 else:
2346 parameters = json.loads(job.arguments)
2347 return parameters[name]
2348
Clark Boylanb640e052014-04-03 16:41:46 -07002349 def haveAllBuildsReported(self):
2350 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002351 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002352 return False
2353 # Find out if every build that the worker has completed has been
2354 # reported back to Zuul. If it hasn't then that means a Gearman
2355 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002356 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002357 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002358 if not zbuild:
2359 # It has already been reported
2360 continue
2361 # It hasn't been reported yet.
2362 return False
2363 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002364 worker = self.executor_server.executor_worker
2365 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002366 if connection.state == 'GRAB_WAIT':
2367 return False
2368 return True
2369
2370 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002371 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002372 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002373 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002374 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002375 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002376 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002377 for j in conn.related_jobs.values():
2378 if j.unique == build.uuid:
2379 client_job = j
2380 break
2381 if not client_job:
2382 self.log.debug("%s is not known to the gearman client" %
2383 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002384 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002385 if not client_job.handle:
2386 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002387 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002388 server_job = self.gearman_server.jobs.get(client_job.handle)
2389 if not server_job:
2390 self.log.debug("%s is not known to the gearman server" %
2391 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002392 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002393 if not hasattr(server_job, 'waiting'):
2394 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002395 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002396 if server_job.waiting:
2397 continue
James E. Blair17302972016-08-10 16:11:42 -07002398 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002399 self.log.debug("%s has not reported start" % build)
2400 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002401 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002402 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002403 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002404 if worker_build:
2405 if worker_build.isWaiting():
2406 continue
2407 else:
2408 self.log.debug("%s is running" % worker_build)
2409 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002410 else:
James E. Blair962220f2016-08-03 11:22:38 -07002411 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002412 return False
James E. Blaira002b032017-04-18 10:35:48 -07002413 for (build_uuid, job_worker) in \
2414 self.executor_server.job_workers.items():
2415 if build_uuid not in seen_builds:
2416 self.log.debug("%s is not finalized" % build_uuid)
2417 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002418 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002419
James E. Blairdce6cea2016-12-20 16:45:32 -08002420 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002421 if self.fake_nodepool.paused:
2422 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002423 if self.sched.nodepool.requests:
2424 return False
2425 return True
2426
Jan Hruban6b71aff2015-10-22 16:58:08 +02002427 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002428 for event_queue in self.event_queues:
2429 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002430
2431 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002432 for event_queue in self.event_queues:
2433 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002434
Clark Boylanb640e052014-04-03 16:41:46 -07002435 def waitUntilSettled(self):
2436 self.log.debug("Waiting until settled...")
2437 start = time.time()
2438 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002439 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002440 self.log.error("Timeout waiting for Zuul to settle")
2441 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002442 for event_queue in self.event_queues:
2443 self.log.error(" %s: %s" %
2444 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002445 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002446 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002447 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002448 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002449 self.log.error("All requests completed: %s" %
2450 (self.areAllNodeRequestsComplete(),))
2451 self.log.error("Merge client jobs: %s" %
2452 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002453 raise Exception("Timeout waiting for Zuul to settle")
2454 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002455
Paul Belanger174a8272017-03-14 13:20:10 -04002456 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002457 # have all build states propogated to zuul?
2458 if self.haveAllBuildsReported():
2459 # Join ensures that the queue is empty _and_ events have been
2460 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002461 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002462 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002463 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002464 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002465 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002466 self.areAllNodeRequestsComplete() and
2467 all(self.eventQueuesEmpty())):
2468 # The queue empty check is placed at the end to
2469 # ensure that if a component adds an event between
2470 # when locked the run handler and checked that the
2471 # components were stable, we don't erroneously
2472 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002473 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002474 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002475 self.log.debug("...settled.")
2476 return
2477 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002478 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002479 self.sched.wake_event.wait(0.1)
2480
2481 def countJobResults(self, jobs, result):
2482 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002483 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002484
Monty Taylor0d926122017-05-24 08:07:56 -05002485 def getBuildByName(self, name):
2486 for build in self.builds:
2487 if build.name == name:
2488 return build
2489 raise Exception("Unable to find build %s" % name)
2490
James E. Blair96c6bf82016-01-15 16:20:40 -08002491 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002492 for job in self.history:
2493 if (job.name == name and
2494 (project is None or
2495 job.parameters['ZUUL_PROJECT'] == project)):
2496 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002497 raise Exception("Unable to find job %s in history" % name)
2498
2499 def assertEmptyQueues(self):
2500 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002501 for tenant in self.sched.abide.tenants.values():
2502 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002503 for pipeline_queue in pipeline.queues:
2504 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002505 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002506 pipeline.name, pipeline_queue.name,
2507 pipeline_queue.queue))
2508 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002509 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002510
2511 def assertReportedStat(self, key, value=None, kind=None):
2512 start = time.time()
2513 while time.time() < (start + 5):
2514 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002515 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002516 if key == k:
2517 if value is None and kind is None:
2518 return
2519 elif value:
2520 if value == v:
2521 return
2522 elif kind:
2523 if v.endswith('|' + kind):
2524 return
2525 time.sleep(0.1)
2526
Clark Boylanb640e052014-04-03 16:41:46 -07002527 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002528
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002529 def assertBuilds(self, builds):
2530 """Assert that the running builds are as described.
2531
2532 The list of running builds is examined and must match exactly
2533 the list of builds described by the input.
2534
2535 :arg list builds: A list of dictionaries. Each item in the
2536 list must match the corresponding build in the build
2537 history, and each element of the dictionary must match the
2538 corresponding attribute of the build.
2539
2540 """
James E. Blair3158e282016-08-19 09:34:11 -07002541 try:
2542 self.assertEqual(len(self.builds), len(builds))
2543 for i, d in enumerate(builds):
2544 for k, v in d.items():
2545 self.assertEqual(
2546 getattr(self.builds[i], k), v,
2547 "Element %i in builds does not match" % (i,))
2548 except Exception:
2549 for build in self.builds:
2550 self.log.error("Running build: %s" % build)
2551 else:
2552 self.log.error("No running builds")
2553 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002554
James E. Blairb536ecc2016-08-31 10:11:42 -07002555 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002556 """Assert that the completed builds are as described.
2557
2558 The list of completed builds is examined and must match
2559 exactly the list of builds described by the input.
2560
2561 :arg list history: A list of dictionaries. Each item in the
2562 list must match the corresponding build in the build
2563 history, and each element of the dictionary must match the
2564 corresponding attribute of the build.
2565
James E. Blairb536ecc2016-08-31 10:11:42 -07002566 :arg bool ordered: If true, the history must match the order
2567 supplied, if false, the builds are permitted to have
2568 arrived in any order.
2569
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002570 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002571 def matches(history_item, item):
2572 for k, v in item.items():
2573 if getattr(history_item, k) != v:
2574 return False
2575 return True
James E. Blair3158e282016-08-19 09:34:11 -07002576 try:
2577 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002578 if ordered:
2579 for i, d in enumerate(history):
2580 if not matches(self.history[i], d):
2581 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002582 "Element %i in history does not match %s" %
2583 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002584 else:
2585 unseen = self.history[:]
2586 for i, d in enumerate(history):
2587 found = False
2588 for unseen_item in unseen:
2589 if matches(unseen_item, d):
2590 found = True
2591 unseen.remove(unseen_item)
2592 break
2593 if not found:
2594 raise Exception("No match found for element %i "
2595 "in history" % (i,))
2596 if unseen:
2597 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002598 except Exception:
2599 for build in self.history:
2600 self.log.error("Completed build: %s" % build)
2601 else:
2602 self.log.error("No completed builds")
2603 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002604
James E. Blair6ac368c2016-12-22 18:07:20 -08002605 def printHistory(self):
2606 """Log the build history.
2607
2608 This can be useful during tests to summarize what jobs have
2609 completed.
2610
2611 """
2612 self.log.debug("Build history:")
2613 for build in self.history:
2614 self.log.debug(build)
2615
James E. Blair59fdbac2015-12-07 17:08:06 -08002616 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002617 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2618
James E. Blair9ea70072017-04-19 16:05:30 -07002619 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002620 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002621 if not os.path.exists(root):
2622 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002623 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2624 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002625- tenant:
2626 name: openstack
2627 source:
2628 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002629 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002630 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002631 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002632 - org/project
2633 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002634 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002635 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002636 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002637 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002638 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002639
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002640 def addCommitToRepo(self, project, message, files,
2641 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002642 path = os.path.join(self.upstream_root, project)
2643 repo = git.Repo(path)
2644 repo.head.reference = branch
2645 zuul.merger.merger.reset_repo_to_head(repo)
2646 for fn, content in files.items():
2647 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002648 try:
2649 os.makedirs(os.path.dirname(fn))
2650 except OSError:
2651 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002652 with open(fn, 'w') as f:
2653 f.write(content)
2654 repo.index.add([fn])
2655 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002656 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002657 repo.heads[branch].commit = commit
2658 repo.head.reference = branch
2659 repo.git.clean('-x', '-f', '-d')
2660 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002661 if tag:
2662 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002663 return before
2664
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002665 def commitConfigUpdate(self, project_name, source_name):
2666 """Commit an update to zuul.yaml
2667
2668 This overwrites the zuul.yaml in the specificed project with
2669 the contents specified.
2670
2671 :arg str project_name: The name of the project containing
2672 zuul.yaml (e.g., common-config)
2673
2674 :arg str source_name: The path to the file (underneath the
2675 test fixture directory) whose contents should be used to
2676 replace zuul.yaml.
2677 """
2678
2679 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002680 files = {}
2681 with open(source_path, 'r') as f:
2682 data = f.read()
2683 layout = yaml.safe_load(data)
2684 files['zuul.yaml'] = data
2685 for item in layout:
2686 if 'job' in item:
2687 jobname = item['job']['name']
2688 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002689 before = self.addCommitToRepo(
2690 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002691 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002692 return before
2693
James E. Blair7fc8daa2016-08-08 15:37:15 -07002694 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002695
James E. Blair7fc8daa2016-08-08 15:37:15 -07002696 """Inject a Fake (Gerrit) event.
2697
2698 This method accepts a JSON-encoded event and simulates Zuul
2699 having received it from Gerrit. It could (and should)
2700 eventually apply to any connection type, but is currently only
2701 used with Gerrit connections. The name of the connection is
2702 used to look up the corresponding server, and the event is
2703 simulated as having been received by all Zuul connections
2704 attached to that server. So if two Gerrit connections in Zuul
2705 are connected to the same Gerrit server, and you invoke this
2706 method specifying the name of one of them, the event will be
2707 received by both.
2708
2709 .. note::
2710
2711 "self.fake_gerrit.addEvent" calls should be migrated to
2712 this method.
2713
2714 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002715 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002716 :arg str event: The JSON-encoded event.
2717
2718 """
2719 specified_conn = self.connections.connections[connection]
2720 for conn in self.connections.connections.values():
2721 if (isinstance(conn, specified_conn.__class__) and
2722 specified_conn.server == conn.server):
2723 conn.addEvent(event)
2724
James E. Blaird8af5422017-05-24 13:59:40 -07002725 def getUpstreamRepos(self, projects):
2726 """Return upstream git repo objects for the listed projects
2727
2728 :arg list projects: A list of strings, each the canonical name
2729 of a project.
2730
2731 :returns: A dictionary of {name: repo} for every listed
2732 project.
2733 :rtype: dict
2734
2735 """
2736
2737 repos = {}
2738 for project in projects:
2739 # FIXME(jeblair): the upstream root does not yet have a
2740 # hostname component; that needs to be added, and this
2741 # line removed:
2742 tmp_project_name = '/'.join(project.split('/')[1:])
2743 path = os.path.join(self.upstream_root, tmp_project_name)
2744 repo = git.Repo(path)
2745 repos[project] = repo
2746 return repos
2747
James E. Blair3f876d52016-07-22 13:07:14 -07002748
2749class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002750 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002751 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002752
Joshua Heskethd78b4482015-09-14 16:56:34 -06002753
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002754class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002755 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002756 use_ssl = True
2757
2758
Joshua Heskethd78b4482015-09-14 16:56:34 -06002759class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002760 def setup_config(self):
2761 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002762 for section_name in self.config.sections():
2763 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2764 section_name, re.I)
2765 if not con_match:
2766 continue
2767
2768 if self.config.get(section_name, 'driver') == 'sql':
2769 f = MySQLSchemaFixture()
2770 self.useFixture(f)
2771 if (self.config.get(section_name, 'dburi') ==
2772 '$MYSQL_FIXTURE_DBURI$'):
2773 self.config.set(section_name, 'dburi', f.dburi)