blob: 617169a614de7b213af727a867e2fed5399d64f0 [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")
James E. Blair01d733e2017-06-23 20:47:51 +01001885 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1886 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001887
1888 if os.path.exists(self.test_root):
1889 shutil.rmtree(self.test_root)
1890 os.makedirs(self.test_root)
1891 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001892 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001893 os.makedirs(self.merger_state_root)
1894 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001895
1896 # Make per test copy of Configuration.
1897 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001898 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1899 if not os.path.exists(self.private_key_file):
1900 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1901 shutil.copy(src_private_key_file, self.private_key_file)
1902 shutil.copy('{}.pub'.format(src_private_key_file),
1903 '{}.pub'.format(self.private_key_file))
1904 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001905 self.config.set('scheduler', 'tenant_config',
1906 os.path.join(
1907 FIXTURE_DIR,
1908 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001909 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001910 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001911 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001912 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001913 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001914 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001915
Clark Boylanb640e052014-04-03 16:41:46 -07001916 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001917 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1918 # see: https://github.com/jsocol/pystatsd/issues/61
1919 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001920 os.environ['STATSD_PORT'] = str(self.statsd.port)
1921 self.statsd.start()
1922 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001923 importlib.reload(statsd)
1924 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001925
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001926 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001927
1928 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001929 self.log.info("Gearman server on port %s" %
1930 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001931 if self.use_ssl:
1932 self.log.info('SSL enabled for gearman')
1933 self.config.set(
1934 'gearman', 'ssl_ca',
1935 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1936 self.config.set(
1937 'gearman', 'ssl_cert',
1938 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1939 self.config.set(
1940 'gearman', 'ssl_key',
1941 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001942
James E. Blaire511d2f2016-12-08 15:22:26 -08001943 gerritsource.GerritSource.replication_timeout = 1.5
1944 gerritsource.GerritSource.replication_retry_interval = 0.5
1945 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001946
Joshua Hesketh352264b2015-08-11 23:42:08 +10001947 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001948
Jan Hruban7083edd2015-08-21 14:00:54 +02001949 self.webapp = zuul.webapp.WebApp(
1950 self.sched, port=0, listen_address='127.0.0.1')
1951
Jan Hruban6b71aff2015-10-22 16:58:08 +02001952 self.event_queues = [
1953 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001954 self.sched.trigger_event_queue,
1955 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001956 ]
1957
James E. Blairfef78942016-03-11 16:28:56 -08001958 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001959 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001960
Paul Belanger174a8272017-03-14 13:20:10 -04001961 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001962 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001963 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001964 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001965 _test_root=self.test_root,
1966 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001967 self.executor_server.start()
1968 self.history = self.executor_server.build_history
1969 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001970
Paul Belanger174a8272017-03-14 13:20:10 -04001971 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001972 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001973 self.merge_client = zuul.merger.client.MergeClient(
1974 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001975 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001976 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001977 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001978
James E. Blair0d5a36e2017-02-21 10:53:44 -05001979 self.fake_nodepool = FakeNodepool(
1980 self.zk_chroot_fixture.zookeeper_host,
1981 self.zk_chroot_fixture.zookeeper_port,
1982 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001983
Paul Belanger174a8272017-03-14 13:20:10 -04001984 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001985 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001986 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001987 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001988
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001989 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001990
1991 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001992 self.webapp.start()
1993 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001994 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001995 # Cleanups are run in reverse order
1996 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001997 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001998 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001999
James E. Blairb9c0d772017-03-03 14:34:49 -08002000 self.sched.reconfigure(self.config)
2001 self.sched.resume()
2002
Tobias Henkel7df274b2017-05-26 17:41:11 +02002003 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002004 # Set up gerrit related fakes
2005 # Set a changes database so multiple FakeGerrit's can report back to
2006 # a virtual canonical database given by the configured hostname
2007 self.gerrit_changes_dbs = {}
2008
2009 def getGerritConnection(driver, name, config):
2010 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2011 con = FakeGerritConnection(driver, name, config,
2012 changes_db=db,
2013 upstream_root=self.upstream_root)
2014 self.event_queues.append(con.event_queue)
2015 setattr(self, 'fake_' + name, con)
2016 return con
2017
2018 self.useFixture(fixtures.MonkeyPatch(
2019 'zuul.driver.gerrit.GerritDriver.getConnection',
2020 getGerritConnection))
2021
Gregory Haynes4fc12542015-04-22 20:38:06 -07002022 def getGithubConnection(driver, name, config):
2023 con = FakeGithubConnection(driver, name, config,
2024 upstream_root=self.upstream_root)
2025 setattr(self, 'fake_' + name, con)
2026 return con
2027
2028 self.useFixture(fixtures.MonkeyPatch(
2029 'zuul.driver.github.GithubDriver.getConnection',
2030 getGithubConnection))
2031
James E. Blaire511d2f2016-12-08 15:22:26 -08002032 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002033 # TODO(jhesketh): This should come from lib.connections for better
2034 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002035 # Register connections from the config
2036 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002037
Joshua Hesketh352264b2015-08-11 23:42:08 +10002038 def FakeSMTPFactory(*args, **kw):
2039 args = [self.smtp_messages] + list(args)
2040 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002041
Joshua Hesketh352264b2015-08-11 23:42:08 +10002042 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002043
James E. Blaire511d2f2016-12-08 15:22:26 -08002044 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002045 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002046 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002047
James E. Blair83005782015-12-11 14:46:03 -08002048 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002049 # This creates the per-test configuration object. It can be
2050 # overriden by subclasses, but should not need to be since it
2051 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002052 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002053 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002054
James E. Blair39840362017-06-23 20:34:02 +01002055 sections = ['zuul', 'scheduler', 'executor', 'merger']
2056 for section in sections:
2057 if not self.config.has_section(section):
2058 self.config.add_section(section)
2059
James E. Blair06cc3922017-04-19 10:08:10 -07002060 if not self.setupSimpleLayout():
2061 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002062 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002063 self.tenant_config_file)
2064 git_path = os.path.join(
2065 os.path.dirname(
2066 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2067 'git')
2068 if os.path.exists(git_path):
2069 for reponame in os.listdir(git_path):
2070 project = reponame.replace('_', '/')
2071 self.copyDirToRepo(project,
2072 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002073 # Make test_root persist after ansible run for .flag test
2074 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002075 self.setupAllProjectKeys()
2076
James E. Blair06cc3922017-04-19 10:08:10 -07002077 def setupSimpleLayout(self):
2078 # If the test method has been decorated with a simple_layout,
2079 # use that instead of the class tenant_config_file. Set up a
2080 # single config-project with the specified layout, and
2081 # initialize repos for all of the 'project' entries which
2082 # appear in the layout.
2083 test_name = self.id().split('.')[-1]
2084 test = getattr(self, test_name)
2085 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002086 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002087 else:
2088 return False
2089
James E. Blairb70e55a2017-04-19 12:57:02 -07002090 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002091 path = os.path.join(FIXTURE_DIR, path)
2092 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002093 data = f.read()
2094 layout = yaml.safe_load(data)
2095 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002096 untrusted_projects = []
2097 for item in layout:
2098 if 'project' in item:
2099 name = item['project']['name']
2100 untrusted_projects.append(name)
2101 self.init_repo(name)
2102 self.addCommitToRepo(name, 'initial commit',
2103 files={'README': ''},
2104 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002105 if 'job' in item:
2106 jobname = item['job']['name']
2107 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002108
2109 root = os.path.join(self.test_root, "config")
2110 if not os.path.exists(root):
2111 os.makedirs(root)
2112 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2113 config = [{'tenant':
2114 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002115 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002116 {'config-projects': ['common-config'],
2117 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002118 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002119 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002120 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002121 os.path.join(FIXTURE_DIR, f.name))
2122
2123 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002124 self.addCommitToRepo('common-config', 'add content from fixture',
2125 files, branch='master', tag='init')
2126
2127 return True
2128
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002129 def setupAllProjectKeys(self):
2130 if self.create_project_keys:
2131 return
2132
James E. Blair39840362017-06-23 20:34:02 +01002133 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002134 with open(os.path.join(FIXTURE_DIR, path)) as f:
2135 tenant_config = yaml.safe_load(f.read())
2136 for tenant in tenant_config:
2137 sources = tenant['tenant']['source']
2138 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002139 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002140 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002141 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002142 self.setupProjectKeys(source, project)
2143
2144 def setupProjectKeys(self, source, project):
2145 # Make sure we set up an RSA key for the project so that we
2146 # don't spend time generating one:
2147
2148 key_root = os.path.join(self.state_root, 'keys')
2149 if not os.path.isdir(key_root):
2150 os.mkdir(key_root, 0o700)
2151 private_key_file = os.path.join(key_root, source, project + '.pem')
2152 private_key_dir = os.path.dirname(private_key_file)
2153 self.log.debug("Installing test keys for project %s at %s" % (
2154 project, private_key_file))
2155 if not os.path.isdir(private_key_dir):
2156 os.makedirs(private_key_dir)
2157 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2158 with open(private_key_file, 'w') as o:
2159 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002160
James E. Blair498059b2016-12-20 13:50:13 -08002161 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002162 self.zk_chroot_fixture = self.useFixture(
2163 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002164 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002165 self.zk_chroot_fixture.zookeeper_host,
2166 self.zk_chroot_fixture.zookeeper_port,
2167 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002168
James E. Blair96c6bf82016-01-15 16:20:40 -08002169 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002170 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002171
2172 files = {}
2173 for (dirpath, dirnames, filenames) in os.walk(source_path):
2174 for filename in filenames:
2175 test_tree_filepath = os.path.join(dirpath, filename)
2176 common_path = os.path.commonprefix([test_tree_filepath,
2177 source_path])
2178 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2179 with open(test_tree_filepath, 'r') as f:
2180 content = f.read()
2181 files[relative_filepath] = content
2182 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002183 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002184
James E. Blaire18d4602017-01-05 11:17:28 -08002185 def assertNodepoolState(self):
2186 # Make sure that there are no pending requests
2187
2188 requests = self.fake_nodepool.getNodeRequests()
2189 self.assertEqual(len(requests), 0)
2190
2191 nodes = self.fake_nodepool.getNodes()
2192 for node in nodes:
2193 self.assertFalse(node['_lock'], "Node %s is locked" %
2194 (node['_oid'],))
2195
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002196 def assertNoGeneratedKeys(self):
2197 # Make sure that Zuul did not generate any project keys
2198 # (unless it was supposed to).
2199
2200 if self.create_project_keys:
2201 return
2202
2203 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2204 test_key = i.read()
2205
2206 key_root = os.path.join(self.state_root, 'keys')
2207 for root, dirname, files in os.walk(key_root):
2208 for fn in files:
2209 with open(os.path.join(root, fn)) as f:
2210 self.assertEqual(test_key, f.read())
2211
Clark Boylanb640e052014-04-03 16:41:46 -07002212 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002213 self.log.debug("Assert final state")
2214 # Make sure no jobs are running
2215 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002216 # Make sure that git.Repo objects have been garbage collected.
2217 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002218 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002219 gc.collect()
2220 for obj in gc.get_objects():
2221 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002222 self.log.debug("Leaked git repo object: 0x%x %s" %
2223 (id(obj), repr(obj)))
2224 for ref in gc.get_referrers(obj):
2225 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002226 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002227 if repos:
2228 for obj in gc.garbage:
2229 self.log.debug(" Garbage %s" % (repr(obj)))
2230 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002231 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002232 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002233 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002234 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002235 for tenant in self.sched.abide.tenants.values():
2236 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002237 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002238 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002239
2240 def shutdown(self):
2241 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002242 self.executor_server.hold_jobs_in_build = False
2243 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002244 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002245 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002246 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002247 self.sched.stop()
2248 self.sched.join()
2249 self.statsd.stop()
2250 self.statsd.join()
2251 self.webapp.stop()
2252 self.webapp.join()
2253 self.rpc.stop()
2254 self.rpc.join()
2255 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002256 self.fake_nodepool.stop()
2257 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002258 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002259 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002260 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002261 # Further the pydevd threads also need to be whitelisted so debugging
2262 # e.g. in PyCharm is possible without breaking shutdown.
2263 whitelist = ['executor-watchdog',
2264 'pydevd.CommandThread',
2265 'pydevd.Reader',
2266 'pydevd.Writer',
2267 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002268 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002269 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002270 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002271 log_str = ""
2272 for thread_id, stack_frame in sys._current_frames().items():
2273 log_str += "Thread: %s\n" % thread_id
2274 log_str += "".join(traceback.format_stack(stack_frame))
2275 self.log.debug(log_str)
2276 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002277
James E. Blaira002b032017-04-18 10:35:48 -07002278 def assertCleanShutdown(self):
2279 pass
2280
James E. Blairc4ba97a2017-04-19 16:26:24 -07002281 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002282 parts = project.split('/')
2283 path = os.path.join(self.upstream_root, *parts[:-1])
2284 if not os.path.exists(path):
2285 os.makedirs(path)
2286 path = os.path.join(self.upstream_root, project)
2287 repo = git.Repo.init(path)
2288
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002289 with repo.config_writer() as config_writer:
2290 config_writer.set_value('user', 'email', 'user@example.com')
2291 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002292
Clark Boylanb640e052014-04-03 16:41:46 -07002293 repo.index.commit('initial commit')
2294 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002295 if tag:
2296 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002297
James E. Blair97d902e2014-08-21 13:25:56 -07002298 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002299 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002300 repo.git.clean('-x', '-f', '-d')
2301
James E. Blair97d902e2014-08-21 13:25:56 -07002302 def create_branch(self, project, branch):
2303 path = os.path.join(self.upstream_root, project)
2304 repo = git.Repo.init(path)
2305 fn = os.path.join(path, 'README')
2306
2307 branch_head = repo.create_head(branch)
2308 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002309 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002310 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002311 f.close()
2312 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002313 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002314
James E. Blair97d902e2014-08-21 13:25:56 -07002315 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002316 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002317 repo.git.clean('-x', '-f', '-d')
2318
Sachi King9f16d522016-03-16 12:20:45 +11002319 def create_commit(self, project):
2320 path = os.path.join(self.upstream_root, project)
2321 repo = git.Repo(path)
2322 repo.head.reference = repo.heads['master']
2323 file_name = os.path.join(path, 'README')
2324 with open(file_name, 'a') as f:
2325 f.write('creating fake commit\n')
2326 repo.index.add([file_name])
2327 commit = repo.index.commit('Creating a fake commit')
2328 return commit.hexsha
2329
James E. Blairf4a5f022017-04-18 14:01:10 -07002330 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002331 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002332 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002333 while len(self.builds):
2334 self.release(self.builds[0])
2335 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002336 i += 1
2337 if count is not None and i >= count:
2338 break
James E. Blairb8c16472015-05-05 14:55:26 -07002339
Clark Boylanb640e052014-04-03 16:41:46 -07002340 def release(self, job):
2341 if isinstance(job, FakeBuild):
2342 job.release()
2343 else:
2344 job.waiting = False
2345 self.log.debug("Queued job %s released" % job.unique)
2346 self.gearman_server.wakeConnections()
2347
2348 def getParameter(self, job, name):
2349 if isinstance(job, FakeBuild):
2350 return job.parameters[name]
2351 else:
2352 parameters = json.loads(job.arguments)
2353 return parameters[name]
2354
Clark Boylanb640e052014-04-03 16:41:46 -07002355 def haveAllBuildsReported(self):
2356 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002357 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002358 return False
2359 # Find out if every build that the worker has completed has been
2360 # reported back to Zuul. If it hasn't then that means a Gearman
2361 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002362 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002363 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002364 if not zbuild:
2365 # It has already been reported
2366 continue
2367 # It hasn't been reported yet.
2368 return False
2369 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002370 worker = self.executor_server.executor_worker
2371 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002372 if connection.state == 'GRAB_WAIT':
2373 return False
2374 return True
2375
2376 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002377 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002378 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002379 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002380 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002381 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002382 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002383 for j in conn.related_jobs.values():
2384 if j.unique == build.uuid:
2385 client_job = j
2386 break
2387 if not client_job:
2388 self.log.debug("%s is not known to the gearman client" %
2389 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002390 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002391 if not client_job.handle:
2392 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002393 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002394 server_job = self.gearman_server.jobs.get(client_job.handle)
2395 if not server_job:
2396 self.log.debug("%s is not known to the gearman server" %
2397 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002398 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002399 if not hasattr(server_job, 'waiting'):
2400 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002401 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002402 if server_job.waiting:
2403 continue
James E. Blair17302972016-08-10 16:11:42 -07002404 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002405 self.log.debug("%s has not reported start" % build)
2406 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002407 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002408 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002409 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002410 if worker_build:
2411 if worker_build.isWaiting():
2412 continue
2413 else:
2414 self.log.debug("%s is running" % worker_build)
2415 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002416 else:
James E. Blair962220f2016-08-03 11:22:38 -07002417 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002418 return False
James E. Blaira002b032017-04-18 10:35:48 -07002419 for (build_uuid, job_worker) in \
2420 self.executor_server.job_workers.items():
2421 if build_uuid not in seen_builds:
2422 self.log.debug("%s is not finalized" % build_uuid)
2423 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002424 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002425
James E. Blairdce6cea2016-12-20 16:45:32 -08002426 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002427 if self.fake_nodepool.paused:
2428 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002429 if self.sched.nodepool.requests:
2430 return False
2431 return True
2432
Jan Hruban6b71aff2015-10-22 16:58:08 +02002433 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002434 for event_queue in self.event_queues:
2435 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002436
2437 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002438 for event_queue in self.event_queues:
2439 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002440
Clark Boylanb640e052014-04-03 16:41:46 -07002441 def waitUntilSettled(self):
2442 self.log.debug("Waiting until settled...")
2443 start = time.time()
2444 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002445 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002446 self.log.error("Timeout waiting for Zuul to settle")
2447 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002448 for event_queue in self.event_queues:
2449 self.log.error(" %s: %s" %
2450 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002451 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002452 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002453 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002454 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002455 self.log.error("All requests completed: %s" %
2456 (self.areAllNodeRequestsComplete(),))
2457 self.log.error("Merge client jobs: %s" %
2458 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002459 raise Exception("Timeout waiting for Zuul to settle")
2460 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002461
Paul Belanger174a8272017-03-14 13:20:10 -04002462 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002463 # have all build states propogated to zuul?
2464 if self.haveAllBuildsReported():
2465 # Join ensures that the queue is empty _and_ events have been
2466 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002467 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002468 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002469 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002470 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002471 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002472 self.areAllNodeRequestsComplete() and
2473 all(self.eventQueuesEmpty())):
2474 # The queue empty check is placed at the end to
2475 # ensure that if a component adds an event between
2476 # when locked the run handler and checked that the
2477 # components were stable, we don't erroneously
2478 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002479 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002480 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002481 self.log.debug("...settled.")
2482 return
2483 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002484 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002485 self.sched.wake_event.wait(0.1)
2486
2487 def countJobResults(self, jobs, result):
2488 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002489 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002490
Monty Taylor0d926122017-05-24 08:07:56 -05002491 def getBuildByName(self, name):
2492 for build in self.builds:
2493 if build.name == name:
2494 return build
2495 raise Exception("Unable to find build %s" % name)
2496
James E. Blair96c6bf82016-01-15 16:20:40 -08002497 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002498 for job in self.history:
2499 if (job.name == name and
2500 (project is None or
2501 job.parameters['ZUUL_PROJECT'] == project)):
2502 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002503 raise Exception("Unable to find job %s in history" % name)
2504
2505 def assertEmptyQueues(self):
2506 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002507 for tenant in self.sched.abide.tenants.values():
2508 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002509 for pipeline_queue in pipeline.queues:
2510 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002511 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002512 pipeline.name, pipeline_queue.name,
2513 pipeline_queue.queue))
2514 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002515 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002516
2517 def assertReportedStat(self, key, value=None, kind=None):
2518 start = time.time()
2519 while time.time() < (start + 5):
2520 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002521 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002522 if key == k:
2523 if value is None and kind is None:
2524 return
2525 elif value:
2526 if value == v:
2527 return
2528 elif kind:
2529 if v.endswith('|' + kind):
2530 return
2531 time.sleep(0.1)
2532
Clark Boylanb640e052014-04-03 16:41:46 -07002533 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002534
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002535 def assertBuilds(self, builds):
2536 """Assert that the running builds are as described.
2537
2538 The list of running builds is examined and must match exactly
2539 the list of builds described by the input.
2540
2541 :arg list builds: A list of dictionaries. Each item in the
2542 list must match the corresponding build in the build
2543 history, and each element of the dictionary must match the
2544 corresponding attribute of the build.
2545
2546 """
James E. Blair3158e282016-08-19 09:34:11 -07002547 try:
2548 self.assertEqual(len(self.builds), len(builds))
2549 for i, d in enumerate(builds):
2550 for k, v in d.items():
2551 self.assertEqual(
2552 getattr(self.builds[i], k), v,
2553 "Element %i in builds does not match" % (i,))
2554 except Exception:
2555 for build in self.builds:
2556 self.log.error("Running build: %s" % build)
2557 else:
2558 self.log.error("No running builds")
2559 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002560
James E. Blairb536ecc2016-08-31 10:11:42 -07002561 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002562 """Assert that the completed builds are as described.
2563
2564 The list of completed builds is examined and must match
2565 exactly the list of builds described by the input.
2566
2567 :arg list history: A list of dictionaries. Each item in the
2568 list must match the corresponding build in the build
2569 history, and each element of the dictionary must match the
2570 corresponding attribute of the build.
2571
James E. Blairb536ecc2016-08-31 10:11:42 -07002572 :arg bool ordered: If true, the history must match the order
2573 supplied, if false, the builds are permitted to have
2574 arrived in any order.
2575
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002576 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002577 def matches(history_item, item):
2578 for k, v in item.items():
2579 if getattr(history_item, k) != v:
2580 return False
2581 return True
James E. Blair3158e282016-08-19 09:34:11 -07002582 try:
2583 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002584 if ordered:
2585 for i, d in enumerate(history):
2586 if not matches(self.history[i], d):
2587 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002588 "Element %i in history does not match %s" %
2589 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002590 else:
2591 unseen = self.history[:]
2592 for i, d in enumerate(history):
2593 found = False
2594 for unseen_item in unseen:
2595 if matches(unseen_item, d):
2596 found = True
2597 unseen.remove(unseen_item)
2598 break
2599 if not found:
2600 raise Exception("No match found for element %i "
2601 "in history" % (i,))
2602 if unseen:
2603 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002604 except Exception:
2605 for build in self.history:
2606 self.log.error("Completed build: %s" % build)
2607 else:
2608 self.log.error("No completed builds")
2609 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002610
James E. Blair6ac368c2016-12-22 18:07:20 -08002611 def printHistory(self):
2612 """Log the build history.
2613
2614 This can be useful during tests to summarize what jobs have
2615 completed.
2616
2617 """
2618 self.log.debug("Build history:")
2619 for build in self.history:
2620 self.log.debug(build)
2621
James E. Blair59fdbac2015-12-07 17:08:06 -08002622 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002623 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2624
James E. Blair9ea70072017-04-19 16:05:30 -07002625 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002626 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002627 if not os.path.exists(root):
2628 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002629 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2630 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002631- tenant:
2632 name: openstack
2633 source:
2634 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002635 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002636 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002637 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002638 - org/project
2639 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002640 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002641 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002642 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002643 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002644 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002645
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002646 def addCommitToRepo(self, project, message, files,
2647 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002648 path = os.path.join(self.upstream_root, project)
2649 repo = git.Repo(path)
2650 repo.head.reference = branch
2651 zuul.merger.merger.reset_repo_to_head(repo)
2652 for fn, content in files.items():
2653 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002654 try:
2655 os.makedirs(os.path.dirname(fn))
2656 except OSError:
2657 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002658 with open(fn, 'w') as f:
2659 f.write(content)
2660 repo.index.add([fn])
2661 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002662 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002663 repo.heads[branch].commit = commit
2664 repo.head.reference = branch
2665 repo.git.clean('-x', '-f', '-d')
2666 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002667 if tag:
2668 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002669 return before
2670
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002671 def commitConfigUpdate(self, project_name, source_name):
2672 """Commit an update to zuul.yaml
2673
2674 This overwrites the zuul.yaml in the specificed project with
2675 the contents specified.
2676
2677 :arg str project_name: The name of the project containing
2678 zuul.yaml (e.g., common-config)
2679
2680 :arg str source_name: The path to the file (underneath the
2681 test fixture directory) whose contents should be used to
2682 replace zuul.yaml.
2683 """
2684
2685 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002686 files = {}
2687 with open(source_path, 'r') as f:
2688 data = f.read()
2689 layout = yaml.safe_load(data)
2690 files['zuul.yaml'] = data
2691 for item in layout:
2692 if 'job' in item:
2693 jobname = item['job']['name']
2694 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002695 before = self.addCommitToRepo(
2696 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002697 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002698 return before
2699
James E. Blair7fc8daa2016-08-08 15:37:15 -07002700 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002701
James E. Blair7fc8daa2016-08-08 15:37:15 -07002702 """Inject a Fake (Gerrit) event.
2703
2704 This method accepts a JSON-encoded event and simulates Zuul
2705 having received it from Gerrit. It could (and should)
2706 eventually apply to any connection type, but is currently only
2707 used with Gerrit connections. The name of the connection is
2708 used to look up the corresponding server, and the event is
2709 simulated as having been received by all Zuul connections
2710 attached to that server. So if two Gerrit connections in Zuul
2711 are connected to the same Gerrit server, and you invoke this
2712 method specifying the name of one of them, the event will be
2713 received by both.
2714
2715 .. note::
2716
2717 "self.fake_gerrit.addEvent" calls should be migrated to
2718 this method.
2719
2720 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002721 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002722 :arg str event: The JSON-encoded event.
2723
2724 """
2725 specified_conn = self.connections.connections[connection]
2726 for conn in self.connections.connections.values():
2727 if (isinstance(conn, specified_conn.__class__) and
2728 specified_conn.server == conn.server):
2729 conn.addEvent(event)
2730
James E. Blaird8af5422017-05-24 13:59:40 -07002731 def getUpstreamRepos(self, projects):
2732 """Return upstream git repo objects for the listed projects
2733
2734 :arg list projects: A list of strings, each the canonical name
2735 of a project.
2736
2737 :returns: A dictionary of {name: repo} for every listed
2738 project.
2739 :rtype: dict
2740
2741 """
2742
2743 repos = {}
2744 for project in projects:
2745 # FIXME(jeblair): the upstream root does not yet have a
2746 # hostname component; that needs to be added, and this
2747 # line removed:
2748 tmp_project_name = '/'.join(project.split('/')[1:])
2749 path = os.path.join(self.upstream_root, tmp_project_name)
2750 repo = git.Repo(path)
2751 repos[project] = repo
2752 return repos
2753
James E. Blair3f876d52016-07-22 13:07:14 -07002754
2755class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002756 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002757 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002758
Joshua Heskethd78b4482015-09-14 16:56:34 -06002759
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002760class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002761 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002762 use_ssl = True
2763
2764
Joshua Heskethd78b4482015-09-14 16:56:34 -06002765class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002766 def setup_config(self):
2767 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002768 for section_name in self.config.sections():
2769 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2770 section_name, re.I)
2771 if not con_match:
2772 continue
2773
2774 if self.config.get(section_name, 'driver') == 'sql':
2775 f = MySQLSchemaFixture()
2776 self.useFixture(f)
2777 if (self.config.get(section_name, 'dburi') ==
2778 '$MYSQL_FIXTURE_DBURI$'):
2779 self.config.set(section_name, 'dburi', f.dburi)