blob: 61e1deb8afec8af141c1ec763e314c4d13c62b67 [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'],
James E. Blairdb93b302017-07-19 15:33:11 -0700413 'ref': self.patchsets[-1]['ref'],
414 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700415 }
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 Keating152a4022017-07-07 08:39:52 -0700557 writers=[], body=None):
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=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700883 body=None):
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)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000925 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926
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" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001051 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001052 change.number))
1053 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001054 if not pr.body:
1055 body = ''
1056 else:
1057 body = pr.body
1058 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001059 # Get our version of a pull so that it's a dict
1060 pull = self.getPull(pr.project, pr.number)
1061 prs.append(pull)
1062
1063 return prs
1064
Gregory Haynes4fc12542015-04-22 20:38:06 -07001065
Clark Boylanb640e052014-04-03 16:41:46 -07001066class BuildHistory(object):
1067 def __init__(self, **kw):
1068 self.__dict__.update(kw)
1069
1070 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001071 return ("<Completed build, result: %s name: %s uuid: %s "
1072 "changes: %s ref: %s>" %
1073 (self.result, self.name, self.uuid,
1074 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001075
1076
Clark Boylanb640e052014-04-03 16:41:46 -07001077class FakeStatsd(threading.Thread):
1078 def __init__(self):
1079 threading.Thread.__init__(self)
1080 self.daemon = True
1081 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1082 self.sock.bind(('', 0))
1083 self.port = self.sock.getsockname()[1]
1084 self.wake_read, self.wake_write = os.pipe()
1085 self.stats = []
1086
1087 def run(self):
1088 while True:
1089 poll = select.poll()
1090 poll.register(self.sock, select.POLLIN)
1091 poll.register(self.wake_read, select.POLLIN)
1092 ret = poll.poll()
1093 for (fd, event) in ret:
1094 if fd == self.sock.fileno():
1095 data = self.sock.recvfrom(1024)
1096 if not data:
1097 return
1098 self.stats.append(data[0])
1099 if fd == self.wake_read:
1100 return
1101
1102 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001103 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001104
1105
James E. Blaire1767bc2016-08-02 10:00:27 -07001106class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001107 log = logging.getLogger("zuul.test")
1108
Paul Belanger174a8272017-03-14 13:20:10 -04001109 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001110 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001111 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001112 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001113 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001114 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001115 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001116 # TODOv3(jeblair): self.node is really "the label of the node
1117 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001118 # keep using it like this, or we may end up exposing more of
1119 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001120 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001121 self.node = None
1122 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001123 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001124 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001125 self.pipeline = self.parameters['ZUUL_PIPELINE']
1126 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001127 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001128 self.wait_condition = threading.Condition()
1129 self.waiting = False
1130 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001131 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001132 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001133 self.changes = None
1134 if 'ZUUL_CHANGE_IDS' in self.parameters:
1135 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001136
James E. Blair3158e282016-08-19 09:34:11 -07001137 def __repr__(self):
1138 waiting = ''
1139 if self.waiting:
1140 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001141 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1142 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001143
Clark Boylanb640e052014-04-03 16:41:46 -07001144 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001145 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001146 self.wait_condition.acquire()
1147 self.wait_condition.notify()
1148 self.waiting = False
1149 self.log.debug("Build %s released" % self.unique)
1150 self.wait_condition.release()
1151
1152 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001153 """Return whether this build is being held.
1154
1155 :returns: Whether the build is being held.
1156 :rtype: bool
1157 """
1158
Clark Boylanb640e052014-04-03 16:41:46 -07001159 self.wait_condition.acquire()
1160 if self.waiting:
1161 ret = True
1162 else:
1163 ret = False
1164 self.wait_condition.release()
1165 return ret
1166
1167 def _wait(self):
1168 self.wait_condition.acquire()
1169 self.waiting = True
1170 self.log.debug("Build %s waiting" % self.unique)
1171 self.wait_condition.wait()
1172 self.wait_condition.release()
1173
1174 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001175 self.log.debug('Running build %s' % self.unique)
1176
Paul Belanger174a8272017-03-14 13:20:10 -04001177 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001178 self.log.debug('Holding build %s' % self.unique)
1179 self._wait()
1180 self.log.debug("Build %s continuing" % self.unique)
1181
James E. Blair412fba82017-01-26 15:00:50 -08001182 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001183 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001184 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001185 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001186 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001187 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001188 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001189
James E. Blaire1767bc2016-08-02 10:00:27 -07001190 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001191
James E. Blaira5dba232016-08-08 15:53:24 -07001192 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001193 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001194 for change in changes:
1195 if self.hasChanges(change):
1196 return True
1197 return False
1198
James E. Blaire7b99a02016-08-05 14:27:34 -07001199 def hasChanges(self, *changes):
1200 """Return whether this build has certain changes in its git repos.
1201
1202 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001203 are expected to be present (in order) in the git repository of
1204 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001205
1206 :returns: Whether the build has the indicated changes.
1207 :rtype: bool
1208
1209 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001210 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001211 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001212 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001213 try:
1214 repo = git.Repo(path)
1215 except NoSuchPathError as e:
1216 self.log.debug('%s' % e)
1217 return False
James E. Blair247cab72017-07-20 16:52:36 -07001218 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001219 commit_message = '%s-1' % change.subject
1220 self.log.debug("Checking if build %s has changes; commit_message "
1221 "%s; repo_messages %s" % (self, commit_message,
1222 repo_messages))
1223 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001224 self.log.debug(" messages do not match")
1225 return False
1226 self.log.debug(" OK")
1227 return True
1228
James E. Blaird8af5422017-05-24 13:59:40 -07001229 def getWorkspaceRepos(self, projects):
1230 """Return workspace git repo objects for the listed projects
1231
1232 :arg list projects: A list of strings, each the canonical name
1233 of a project.
1234
1235 :returns: A dictionary of {name: repo} for every listed
1236 project.
1237 :rtype: dict
1238
1239 """
1240
1241 repos = {}
1242 for project in projects:
1243 path = os.path.join(self.jobdir.src_root, project)
1244 repo = git.Repo(path)
1245 repos[project] = repo
1246 return repos
1247
Clark Boylanb640e052014-04-03 16:41:46 -07001248
Paul Belanger174a8272017-03-14 13:20:10 -04001249class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1250 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001251
Paul Belanger174a8272017-03-14 13:20:10 -04001252 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001253 they will report that they have started but then pause until
1254 released before reporting completion. This attribute may be
1255 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001256 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001257 be explicitly released.
1258
1259 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001260 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001261 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001262 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001263 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001264 self.hold_jobs_in_build = False
1265 self.lock = threading.Lock()
1266 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001267 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001268 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001269 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001270
James E. Blaira5dba232016-08-08 15:53:24 -07001271 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001272 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001273
1274 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001275 :arg Change change: The :py:class:`~tests.base.FakeChange`
1276 instance which should cause the job to fail. This job
1277 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001278
1279 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001280 l = self.fail_tests.get(name, [])
1281 l.append(change)
1282 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001283
James E. Blair962220f2016-08-03 11:22:38 -07001284 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001285 """Release a held build.
1286
1287 :arg str regex: A regular expression which, if supplied, will
1288 cause only builds with matching names to be released. If
1289 not supplied, all builds will be released.
1290
1291 """
James E. Blair962220f2016-08-03 11:22:38 -07001292 builds = self.running_builds[:]
1293 self.log.debug("Releasing build %s (%s)" % (regex,
1294 len(self.running_builds)))
1295 for build in builds:
1296 if not regex or re.match(regex, build.name):
1297 self.log.debug("Releasing build %s" %
1298 (build.parameters['ZUUL_UUID']))
1299 build.release()
1300 else:
1301 self.log.debug("Not releasing build %s" %
1302 (build.parameters['ZUUL_UUID']))
1303 self.log.debug("Done releasing builds %s (%s)" %
1304 (regex, len(self.running_builds)))
1305
Paul Belanger174a8272017-03-14 13:20:10 -04001306 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001307 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001308 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001309 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001310 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001311 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001312 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001313 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001314 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1315 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001316
1317 def stopJob(self, job):
1318 self.log.debug("handle stop")
1319 parameters = json.loads(job.arguments)
1320 uuid = parameters['uuid']
1321 for build in self.running_builds:
1322 if build.unique == uuid:
1323 build.aborted = True
1324 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001325 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001326
James E. Blaira002b032017-04-18 10:35:48 -07001327 def stop(self):
1328 for build in self.running_builds:
1329 build.release()
1330 super(RecordingExecutorServer, self).stop()
1331
Joshua Hesketh50c21782016-10-13 21:34:14 +11001332
Paul Belanger174a8272017-03-14 13:20:10 -04001333class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001334 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001335 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001336 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001337 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001338 if not commit: # merge conflict
1339 self.recordResult('MERGER_FAILURE')
1340 return commit
1341
1342 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001343 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001344 self.executor_server.lock.acquire()
1345 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001346 BuildHistory(name=build.name, result=result, changes=build.changes,
1347 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001348 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001349 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001350 pipeline=build.parameters['ZUUL_PIPELINE'])
1351 )
Paul Belanger174a8272017-03-14 13:20:10 -04001352 self.executor_server.running_builds.remove(build)
1353 del self.executor_server.job_builds[self.job.unique]
1354 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001355
1356 def runPlaybooks(self, args):
1357 build = self.executor_server.job_builds[self.job.unique]
1358 build.jobdir = self.jobdir
1359
1360 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1361 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001362 return result
1363
James E. Blair74a82cf2017-07-12 17:23:08 -07001364 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001365 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001366
Paul Belanger174a8272017-03-14 13:20:10 -04001367 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001368 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001369 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001370 else:
1371 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001372 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001373
James E. Blairad8dca02017-02-21 11:48:32 -05001374 def getHostList(self, args):
1375 self.log.debug("hostlist")
1376 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001377 for host in hosts:
1378 host['host_vars']['ansible_connection'] = 'local'
1379
1380 hosts.append(dict(
1381 name='localhost',
1382 host_vars=dict(ansible_connection='local'),
1383 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001384 return hosts
1385
James E. Blairf5dbd002015-12-23 15:26:17 -08001386
Clark Boylanb640e052014-04-03 16:41:46 -07001387class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001388 """A Gearman server for use in tests.
1389
1390 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1391 added to the queue but will not be distributed to workers
1392 until released. This attribute may be changed at any time and
1393 will take effect for subsequently enqueued jobs, but
1394 previously held jobs will still need to be explicitly
1395 released.
1396
1397 """
1398
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001399 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001400 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001401 if use_ssl:
1402 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1403 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1404 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1405 else:
1406 ssl_ca = None
1407 ssl_cert = None
1408 ssl_key = None
1409
1410 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1411 ssl_cert=ssl_cert,
1412 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001413
1414 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001415 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1416 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001417 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001418 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001419 job.waiting = self.hold_jobs_in_queue
1420 else:
1421 job.waiting = False
1422 if job.waiting:
1423 continue
1424 if job.name in connection.functions:
1425 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001426 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001427 connection.related_jobs[job.handle] = job
1428 job.worker_connection = connection
1429 job.running = True
1430 return job
1431 return None
1432
1433 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001434 """Release a held job.
1435
1436 :arg str regex: A regular expression which, if supplied, will
1437 cause only jobs with matching names to be released. If
1438 not supplied, all jobs will be released.
1439 """
Clark Boylanb640e052014-04-03 16:41:46 -07001440 released = False
1441 qlen = (len(self.high_queue) + len(self.normal_queue) +
1442 len(self.low_queue))
1443 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1444 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001445 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001446 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001447 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001448 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001449 self.log.debug("releasing queued job %s" %
1450 job.unique)
1451 job.waiting = False
1452 released = True
1453 else:
1454 self.log.debug("not releasing queued job %s" %
1455 job.unique)
1456 if released:
1457 self.wakeConnections()
1458 qlen = (len(self.high_queue) + len(self.normal_queue) +
1459 len(self.low_queue))
1460 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1461
1462
1463class FakeSMTP(object):
1464 log = logging.getLogger('zuul.FakeSMTP')
1465
1466 def __init__(self, messages, server, port):
1467 self.server = server
1468 self.port = port
1469 self.messages = messages
1470
1471 def sendmail(self, from_email, to_email, msg):
1472 self.log.info("Sending email from %s, to %s, with msg %s" % (
1473 from_email, to_email, msg))
1474
1475 headers = msg.split('\n\n', 1)[0]
1476 body = msg.split('\n\n', 1)[1]
1477
1478 self.messages.append(dict(
1479 from_email=from_email,
1480 to_email=to_email,
1481 msg=msg,
1482 headers=headers,
1483 body=body,
1484 ))
1485
1486 return True
1487
1488 def quit(self):
1489 return True
1490
1491
James E. Blairdce6cea2016-12-20 16:45:32 -08001492class FakeNodepool(object):
1493 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001494 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001495
1496 log = logging.getLogger("zuul.test.FakeNodepool")
1497
1498 def __init__(self, host, port, chroot):
1499 self.client = kazoo.client.KazooClient(
1500 hosts='%s:%s%s' % (host, port, chroot))
1501 self.client.start()
1502 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001503 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001504 self.thread = threading.Thread(target=self.run)
1505 self.thread.daemon = True
1506 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001507 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001508
1509 def stop(self):
1510 self._running = False
1511 self.thread.join()
1512 self.client.stop()
1513 self.client.close()
1514
1515 def run(self):
1516 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001517 try:
1518 self._run()
1519 except Exception:
1520 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001521 time.sleep(0.1)
1522
1523 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001524 if self.paused:
1525 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001526 for req in self.getNodeRequests():
1527 self.fulfillRequest(req)
1528
1529 def getNodeRequests(self):
1530 try:
1531 reqids = self.client.get_children(self.REQUEST_ROOT)
1532 except kazoo.exceptions.NoNodeError:
1533 return []
1534 reqs = []
1535 for oid in sorted(reqids):
1536 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001537 try:
1538 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001539 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001540 data['_oid'] = oid
1541 reqs.append(data)
1542 except kazoo.exceptions.NoNodeError:
1543 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001544 return reqs
1545
James E. Blaire18d4602017-01-05 11:17:28 -08001546 def getNodes(self):
1547 try:
1548 nodeids = self.client.get_children(self.NODE_ROOT)
1549 except kazoo.exceptions.NoNodeError:
1550 return []
1551 nodes = []
1552 for oid in sorted(nodeids):
1553 path = self.NODE_ROOT + '/' + oid
1554 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001555 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001556 data['_oid'] = oid
1557 try:
1558 lockfiles = self.client.get_children(path + '/lock')
1559 except kazoo.exceptions.NoNodeError:
1560 lockfiles = []
1561 if lockfiles:
1562 data['_lock'] = True
1563 else:
1564 data['_lock'] = False
1565 nodes.append(data)
1566 return nodes
1567
James E. Blaira38c28e2017-01-04 10:33:20 -08001568 def makeNode(self, request_id, node_type):
1569 now = time.time()
1570 path = '/nodepool/nodes/'
1571 data = dict(type=node_type,
1572 provider='test-provider',
1573 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001574 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001575 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001576 public_ipv4='127.0.0.1',
1577 private_ipv4=None,
1578 public_ipv6=None,
1579 allocated_to=request_id,
1580 state='ready',
1581 state_time=now,
1582 created_time=now,
1583 updated_time=now,
1584 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001585 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001586 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001587 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001588 path = self.client.create(path, data,
1589 makepath=True,
1590 sequence=True)
1591 nodeid = path.split("/")[-1]
1592 return nodeid
1593
James E. Blair6ab79e02017-01-06 10:10:17 -08001594 def addFailRequest(self, request):
1595 self.fail_requests.add(request['_oid'])
1596
James E. Blairdce6cea2016-12-20 16:45:32 -08001597 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001598 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001599 return
1600 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001601 oid = request['_oid']
1602 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001603
James E. Blair6ab79e02017-01-06 10:10:17 -08001604 if oid in self.fail_requests:
1605 request['state'] = 'failed'
1606 else:
1607 request['state'] = 'fulfilled'
1608 nodes = []
1609 for node in request['node_types']:
1610 nodeid = self.makeNode(oid, node)
1611 nodes.append(nodeid)
1612 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001613
James E. Blaira38c28e2017-01-04 10:33:20 -08001614 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001615 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001616 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001617 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001618 try:
1619 self.client.set(path, data)
1620 except kazoo.exceptions.NoNodeError:
1621 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001622
1623
James E. Blair498059b2016-12-20 13:50:13 -08001624class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001625 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001626 super(ChrootedKazooFixture, self).__init__()
1627
1628 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1629 if ':' in zk_host:
1630 host, port = zk_host.split(':')
1631 else:
1632 host = zk_host
1633 port = None
1634
1635 self.zookeeper_host = host
1636
1637 if not port:
1638 self.zookeeper_port = 2181
1639 else:
1640 self.zookeeper_port = int(port)
1641
Clark Boylan621ec9a2017-04-07 17:41:33 -07001642 self.test_id = test_id
1643
James E. Blair498059b2016-12-20 13:50:13 -08001644 def _setUp(self):
1645 # Make sure the test chroot paths do not conflict
1646 random_bits = ''.join(random.choice(string.ascii_lowercase +
1647 string.ascii_uppercase)
1648 for x in range(8))
1649
Clark Boylan621ec9a2017-04-07 17:41:33 -07001650 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001651 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1652
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001653 self.addCleanup(self._cleanup)
1654
James E. Blair498059b2016-12-20 13:50:13 -08001655 # Ensure the chroot path exists and clean up any pre-existing znodes.
1656 _tmp_client = kazoo.client.KazooClient(
1657 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1658 _tmp_client.start()
1659
1660 if _tmp_client.exists(self.zookeeper_chroot):
1661 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1662
1663 _tmp_client.ensure_path(self.zookeeper_chroot)
1664 _tmp_client.stop()
1665 _tmp_client.close()
1666
James E. Blair498059b2016-12-20 13:50:13 -08001667 def _cleanup(self):
1668 '''Remove the chroot path.'''
1669 # Need a non-chroot'ed client to remove the chroot path
1670 _tmp_client = kazoo.client.KazooClient(
1671 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1672 _tmp_client.start()
1673 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1674 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001675 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001676
1677
Joshua Heskethd78b4482015-09-14 16:56:34 -06001678class MySQLSchemaFixture(fixtures.Fixture):
1679 def setUp(self):
1680 super(MySQLSchemaFixture, self).setUp()
1681
1682 random_bits = ''.join(random.choice(string.ascii_lowercase +
1683 string.ascii_uppercase)
1684 for x in range(8))
1685 self.name = '%s_%s' % (random_bits, os.getpid())
1686 self.passwd = uuid.uuid4().hex
1687 db = pymysql.connect(host="localhost",
1688 user="openstack_citest",
1689 passwd="openstack_citest",
1690 db="openstack_citest")
1691 cur = db.cursor()
1692 cur.execute("create database %s" % self.name)
1693 cur.execute(
1694 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1695 (self.name, self.name, self.passwd))
1696 cur.execute("flush privileges")
1697
1698 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1699 self.passwd,
1700 self.name)
1701 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1702 self.addCleanup(self.cleanup)
1703
1704 def cleanup(self):
1705 db = pymysql.connect(host="localhost",
1706 user="openstack_citest",
1707 passwd="openstack_citest",
1708 db="openstack_citest")
1709 cur = db.cursor()
1710 cur.execute("drop database %s" % self.name)
1711 cur.execute("drop user '%s'@'localhost'" % self.name)
1712 cur.execute("flush privileges")
1713
1714
Maru Newby3fe5f852015-01-13 04:22:14 +00001715class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001716 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001717 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001718
James E. Blair1c236df2017-02-01 14:07:24 -08001719 def attachLogs(self, *args):
1720 def reader():
1721 self._log_stream.seek(0)
1722 while True:
1723 x = self._log_stream.read(4096)
1724 if not x:
1725 break
1726 yield x.encode('utf8')
1727 content = testtools.content.content_from_reader(
1728 reader,
1729 testtools.content_type.UTF8_TEXT,
1730 False)
1731 self.addDetail('logging', content)
1732
Clark Boylanb640e052014-04-03 16:41:46 -07001733 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001734 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001735 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1736 try:
1737 test_timeout = int(test_timeout)
1738 except ValueError:
1739 # If timeout value is invalid do not set a timeout.
1740 test_timeout = 0
1741 if test_timeout > 0:
1742 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1743
1744 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1745 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1746 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1747 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1748 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1749 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1750 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1751 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1752 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1753 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001754 self._log_stream = StringIO()
1755 self.addOnException(self.attachLogs)
1756 else:
1757 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001758
James E. Blair73b41772017-05-22 13:22:55 -07001759 # NOTE(jeblair): this is temporary extra debugging to try to
1760 # track down a possible leak.
1761 orig_git_repo_init = git.Repo.__init__
1762
1763 def git_repo_init(myself, *args, **kw):
1764 orig_git_repo_init(myself, *args, **kw)
1765 self.log.debug("Created git repo 0x%x %s" %
1766 (id(myself), repr(myself)))
1767
1768 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1769 git_repo_init))
1770
James E. Blair1c236df2017-02-01 14:07:24 -08001771 handler = logging.StreamHandler(self._log_stream)
1772 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1773 '%(levelname)-8s %(message)s')
1774 handler.setFormatter(formatter)
1775
1776 logger = logging.getLogger()
1777 logger.setLevel(logging.DEBUG)
1778 logger.addHandler(handler)
1779
Clark Boylan3410d532017-04-25 12:35:29 -07001780 # Make sure we don't carry old handlers around in process state
1781 # which slows down test runs
1782 self.addCleanup(logger.removeHandler, handler)
1783 self.addCleanup(handler.close)
1784 self.addCleanup(handler.flush)
1785
James E. Blair1c236df2017-02-01 14:07:24 -08001786 # NOTE(notmorgan): Extract logging overrides for specific
1787 # libraries from the OS_LOG_DEFAULTS env and create loggers
1788 # for each. This is used to limit the output during test runs
1789 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001790 log_defaults_from_env = os.environ.get(
1791 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001792 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001793
James E. Blairdce6cea2016-12-20 16:45:32 -08001794 if log_defaults_from_env:
1795 for default in log_defaults_from_env.split(','):
1796 try:
1797 name, level_str = default.split('=', 1)
1798 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001799 logger = logging.getLogger(name)
1800 logger.setLevel(level)
1801 logger.addHandler(handler)
1802 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001803 except ValueError:
1804 # NOTE(notmorgan): Invalid format of the log default,
1805 # skip and don't try and apply a logger for the
1806 # specified module
1807 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001808
Maru Newby3fe5f852015-01-13 04:22:14 +00001809
1810class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001811 """A test case with a functioning Zuul.
1812
1813 The following class variables are used during test setup and can
1814 be overidden by subclasses but are effectively read-only once a
1815 test method starts running:
1816
1817 :cvar str config_file: This points to the main zuul config file
1818 within the fixtures directory. Subclasses may override this
1819 to obtain a different behavior.
1820
1821 :cvar str tenant_config_file: This is the tenant config file
1822 (which specifies from what git repos the configuration should
1823 be loaded). It defaults to the value specified in
1824 `config_file` but can be overidden by subclasses to obtain a
1825 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001826 configuration. See also the :py:func:`simple_layout`
1827 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001828
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001829 :cvar bool create_project_keys: Indicates whether Zuul should
1830 auto-generate keys for each project, or whether the test
1831 infrastructure should insert dummy keys to save time during
1832 startup. Defaults to False.
1833
James E. Blaire7b99a02016-08-05 14:27:34 -07001834 The following are instance variables that are useful within test
1835 methods:
1836
1837 :ivar FakeGerritConnection fake_<connection>:
1838 A :py:class:`~tests.base.FakeGerritConnection` will be
1839 instantiated for each connection present in the config file
1840 and stored here. For instance, `fake_gerrit` will hold the
1841 FakeGerritConnection object for a connection named `gerrit`.
1842
1843 :ivar FakeGearmanServer gearman_server: An instance of
1844 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1845 server that all of the Zuul components in this test use to
1846 communicate with each other.
1847
Paul Belanger174a8272017-03-14 13:20:10 -04001848 :ivar RecordingExecutorServer executor_server: An instance of
1849 :py:class:`~tests.base.RecordingExecutorServer` which is the
1850 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001851
1852 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1853 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001854 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001855 list upon completion.
1856
1857 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1858 objects representing completed builds. They are appended to
1859 the list in the order they complete.
1860
1861 """
1862
James E. Blair83005782015-12-11 14:46:03 -08001863 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001864 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001865 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001866 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001867
1868 def _startMerger(self):
1869 self.merge_server = zuul.merger.server.MergeServer(self.config,
1870 self.connections)
1871 self.merge_server.start()
1872
Maru Newby3fe5f852015-01-13 04:22:14 +00001873 def setUp(self):
1874 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001875
1876 self.setupZK()
1877
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001878 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001879 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001880 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1881 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001882 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001883 tmp_root = tempfile.mkdtemp(
1884 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001885 self.test_root = os.path.join(tmp_root, "zuul-test")
1886 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001887 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001888 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001889 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001890 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1891 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001892
1893 if os.path.exists(self.test_root):
1894 shutil.rmtree(self.test_root)
1895 os.makedirs(self.test_root)
1896 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001897 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001898 os.makedirs(self.merger_state_root)
1899 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001900
1901 # Make per test copy of Configuration.
1902 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001903 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1904 if not os.path.exists(self.private_key_file):
1905 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1906 shutil.copy(src_private_key_file, self.private_key_file)
1907 shutil.copy('{}.pub'.format(src_private_key_file),
1908 '{}.pub'.format(self.private_key_file))
1909 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001910 self.config.set('scheduler', 'tenant_config',
1911 os.path.join(
1912 FIXTURE_DIR,
1913 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001914 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001915 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001916 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001917 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001918 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001919 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001920
Clark Boylanb640e052014-04-03 16:41:46 -07001921 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001922 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1923 # see: https://github.com/jsocol/pystatsd/issues/61
1924 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001925 os.environ['STATSD_PORT'] = str(self.statsd.port)
1926 self.statsd.start()
1927 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001928 importlib.reload(statsd)
1929 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001930
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001931 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001932
1933 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001934 self.log.info("Gearman server on port %s" %
1935 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001936 if self.use_ssl:
1937 self.log.info('SSL enabled for gearman')
1938 self.config.set(
1939 'gearman', 'ssl_ca',
1940 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1941 self.config.set(
1942 'gearman', 'ssl_cert',
1943 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1944 self.config.set(
1945 'gearman', 'ssl_key',
1946 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001947
James E. Blaire511d2f2016-12-08 15:22:26 -08001948 gerritsource.GerritSource.replication_timeout = 1.5
1949 gerritsource.GerritSource.replication_retry_interval = 0.5
1950 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001951
Joshua Hesketh352264b2015-08-11 23:42:08 +10001952 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001953
Jan Hruban7083edd2015-08-21 14:00:54 +02001954 self.webapp = zuul.webapp.WebApp(
1955 self.sched, port=0, listen_address='127.0.0.1')
1956
Jan Hruban6b71aff2015-10-22 16:58:08 +02001957 self.event_queues = [
1958 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001959 self.sched.trigger_event_queue,
1960 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001961 ]
1962
James E. Blairfef78942016-03-11 16:28:56 -08001963 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001964 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001965
Paul Belanger174a8272017-03-14 13:20:10 -04001966 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001967 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001968 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001969 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001970 _test_root=self.test_root,
1971 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001972 self.executor_server.start()
1973 self.history = self.executor_server.build_history
1974 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001975
Paul Belanger174a8272017-03-14 13:20:10 -04001976 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001977 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001978 self.merge_client = zuul.merger.client.MergeClient(
1979 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001980 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001981 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001982 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001983
James E. Blair0d5a36e2017-02-21 10:53:44 -05001984 self.fake_nodepool = FakeNodepool(
1985 self.zk_chroot_fixture.zookeeper_host,
1986 self.zk_chroot_fixture.zookeeper_port,
1987 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001988
Paul Belanger174a8272017-03-14 13:20:10 -04001989 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001990 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001991 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001992 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001993
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001994 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001995
1996 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001997 self.webapp.start()
1998 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001999 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002000 # Cleanups are run in reverse order
2001 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002002 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002003 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002004
James E. Blairb9c0d772017-03-03 14:34:49 -08002005 self.sched.reconfigure(self.config)
2006 self.sched.resume()
2007
Tobias Henkel7df274b2017-05-26 17:41:11 +02002008 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002009 # Set up gerrit related fakes
2010 # Set a changes database so multiple FakeGerrit's can report back to
2011 # a virtual canonical database given by the configured hostname
2012 self.gerrit_changes_dbs = {}
2013
2014 def getGerritConnection(driver, name, config):
2015 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2016 con = FakeGerritConnection(driver, name, config,
2017 changes_db=db,
2018 upstream_root=self.upstream_root)
2019 self.event_queues.append(con.event_queue)
2020 setattr(self, 'fake_' + name, con)
2021 return con
2022
2023 self.useFixture(fixtures.MonkeyPatch(
2024 'zuul.driver.gerrit.GerritDriver.getConnection',
2025 getGerritConnection))
2026
Gregory Haynes4fc12542015-04-22 20:38:06 -07002027 def getGithubConnection(driver, name, config):
2028 con = FakeGithubConnection(driver, name, config,
2029 upstream_root=self.upstream_root)
2030 setattr(self, 'fake_' + name, con)
2031 return con
2032
2033 self.useFixture(fixtures.MonkeyPatch(
2034 'zuul.driver.github.GithubDriver.getConnection',
2035 getGithubConnection))
2036
James E. Blaire511d2f2016-12-08 15:22:26 -08002037 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002038 # TODO(jhesketh): This should come from lib.connections for better
2039 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002040 # Register connections from the config
2041 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002042
Joshua Hesketh352264b2015-08-11 23:42:08 +10002043 def FakeSMTPFactory(*args, **kw):
2044 args = [self.smtp_messages] + list(args)
2045 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002046
Joshua Hesketh352264b2015-08-11 23:42:08 +10002047 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002048
James E. Blaire511d2f2016-12-08 15:22:26 -08002049 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002050 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002051 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002052
James E. Blair83005782015-12-11 14:46:03 -08002053 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002054 # This creates the per-test configuration object. It can be
2055 # overriden by subclasses, but should not need to be since it
2056 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002057 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002058 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002059
James E. Blair39840362017-06-23 20:34:02 +01002060 sections = ['zuul', 'scheduler', 'executor', 'merger']
2061 for section in sections:
2062 if not self.config.has_section(section):
2063 self.config.add_section(section)
2064
James E. Blair06cc3922017-04-19 10:08:10 -07002065 if not self.setupSimpleLayout():
2066 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002067 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002068 self.tenant_config_file)
2069 git_path = os.path.join(
2070 os.path.dirname(
2071 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2072 'git')
2073 if os.path.exists(git_path):
2074 for reponame in os.listdir(git_path):
2075 project = reponame.replace('_', '/')
2076 self.copyDirToRepo(project,
2077 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002078 # Make test_root persist after ansible run for .flag test
2079 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002080 self.setupAllProjectKeys()
2081
James E. Blair06cc3922017-04-19 10:08:10 -07002082 def setupSimpleLayout(self):
2083 # If the test method has been decorated with a simple_layout,
2084 # use that instead of the class tenant_config_file. Set up a
2085 # single config-project with the specified layout, and
2086 # initialize repos for all of the 'project' entries which
2087 # appear in the layout.
2088 test_name = self.id().split('.')[-1]
2089 test = getattr(self, test_name)
2090 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002091 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002092 else:
2093 return False
2094
James E. Blairb70e55a2017-04-19 12:57:02 -07002095 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002096 path = os.path.join(FIXTURE_DIR, path)
2097 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002098 data = f.read()
2099 layout = yaml.safe_load(data)
2100 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002101 untrusted_projects = []
2102 for item in layout:
2103 if 'project' in item:
2104 name = item['project']['name']
2105 untrusted_projects.append(name)
2106 self.init_repo(name)
2107 self.addCommitToRepo(name, 'initial commit',
2108 files={'README': ''},
2109 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002110 if 'job' in item:
2111 jobname = item['job']['name']
2112 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002113
2114 root = os.path.join(self.test_root, "config")
2115 if not os.path.exists(root):
2116 os.makedirs(root)
2117 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2118 config = [{'tenant':
2119 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002120 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002121 {'config-projects': ['common-config'],
2122 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002123 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002124 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002125 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002126 os.path.join(FIXTURE_DIR, f.name))
2127
2128 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002129 self.addCommitToRepo('common-config', 'add content from fixture',
2130 files, branch='master', tag='init')
2131
2132 return True
2133
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002134 def setupAllProjectKeys(self):
2135 if self.create_project_keys:
2136 return
2137
James E. Blair39840362017-06-23 20:34:02 +01002138 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002139 with open(os.path.join(FIXTURE_DIR, path)) as f:
2140 tenant_config = yaml.safe_load(f.read())
2141 for tenant in tenant_config:
2142 sources = tenant['tenant']['source']
2143 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002144 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002145 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002146 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002147 self.setupProjectKeys(source, project)
2148
2149 def setupProjectKeys(self, source, project):
2150 # Make sure we set up an RSA key for the project so that we
2151 # don't spend time generating one:
2152
James E. Blair6459db12017-06-29 14:57:20 -07002153 if isinstance(project, dict):
2154 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002155 key_root = os.path.join(self.state_root, 'keys')
2156 if not os.path.isdir(key_root):
2157 os.mkdir(key_root, 0o700)
2158 private_key_file = os.path.join(key_root, source, project + '.pem')
2159 private_key_dir = os.path.dirname(private_key_file)
2160 self.log.debug("Installing test keys for project %s at %s" % (
2161 project, private_key_file))
2162 if not os.path.isdir(private_key_dir):
2163 os.makedirs(private_key_dir)
2164 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2165 with open(private_key_file, 'w') as o:
2166 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002167
James E. Blair498059b2016-12-20 13:50:13 -08002168 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002169 self.zk_chroot_fixture = self.useFixture(
2170 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002171 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002172 self.zk_chroot_fixture.zookeeper_host,
2173 self.zk_chroot_fixture.zookeeper_port,
2174 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002175
James E. Blair96c6bf82016-01-15 16:20:40 -08002176 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002177 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002178
2179 files = {}
2180 for (dirpath, dirnames, filenames) in os.walk(source_path):
2181 for filename in filenames:
2182 test_tree_filepath = os.path.join(dirpath, filename)
2183 common_path = os.path.commonprefix([test_tree_filepath,
2184 source_path])
2185 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2186 with open(test_tree_filepath, 'r') as f:
2187 content = f.read()
2188 files[relative_filepath] = content
2189 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002190 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002191
James E. Blaire18d4602017-01-05 11:17:28 -08002192 def assertNodepoolState(self):
2193 # Make sure that there are no pending requests
2194
2195 requests = self.fake_nodepool.getNodeRequests()
2196 self.assertEqual(len(requests), 0)
2197
2198 nodes = self.fake_nodepool.getNodes()
2199 for node in nodes:
2200 self.assertFalse(node['_lock'], "Node %s is locked" %
2201 (node['_oid'],))
2202
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002203 def assertNoGeneratedKeys(self):
2204 # Make sure that Zuul did not generate any project keys
2205 # (unless it was supposed to).
2206
2207 if self.create_project_keys:
2208 return
2209
2210 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2211 test_key = i.read()
2212
2213 key_root = os.path.join(self.state_root, 'keys')
2214 for root, dirname, files in os.walk(key_root):
2215 for fn in files:
2216 with open(os.path.join(root, fn)) as f:
2217 self.assertEqual(test_key, f.read())
2218
Clark Boylanb640e052014-04-03 16:41:46 -07002219 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002220 self.log.debug("Assert final state")
2221 # Make sure no jobs are running
2222 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002223 # Make sure that git.Repo objects have been garbage collected.
2224 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002225 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002226 gc.collect()
2227 for obj in gc.get_objects():
2228 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002229 self.log.debug("Leaked git repo object: 0x%x %s" %
2230 (id(obj), repr(obj)))
2231 for ref in gc.get_referrers(obj):
2232 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002233 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002234 if repos:
2235 for obj in gc.garbage:
2236 self.log.debug(" Garbage %s" % (repr(obj)))
2237 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002238 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002239 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002240 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002241 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002242 for tenant in self.sched.abide.tenants.values():
2243 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002244 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002245 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002246
2247 def shutdown(self):
2248 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002249 self.executor_server.hold_jobs_in_build = False
2250 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002251 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002252 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002253 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002254 self.sched.stop()
2255 self.sched.join()
2256 self.statsd.stop()
2257 self.statsd.join()
2258 self.webapp.stop()
2259 self.webapp.join()
2260 self.rpc.stop()
2261 self.rpc.join()
2262 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002263 self.fake_nodepool.stop()
2264 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002265 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002266 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002267 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002268 # Further the pydevd threads also need to be whitelisted so debugging
2269 # e.g. in PyCharm is possible without breaking shutdown.
2270 whitelist = ['executor-watchdog',
2271 'pydevd.CommandThread',
2272 'pydevd.Reader',
2273 'pydevd.Writer',
2274 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002275 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002276 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002277 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002278 log_str = ""
2279 for thread_id, stack_frame in sys._current_frames().items():
2280 log_str += "Thread: %s\n" % thread_id
2281 log_str += "".join(traceback.format_stack(stack_frame))
2282 self.log.debug(log_str)
2283 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002284
James E. Blaira002b032017-04-18 10:35:48 -07002285 def assertCleanShutdown(self):
2286 pass
2287
James E. Blairc4ba97a2017-04-19 16:26:24 -07002288 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002289 parts = project.split('/')
2290 path = os.path.join(self.upstream_root, *parts[:-1])
2291 if not os.path.exists(path):
2292 os.makedirs(path)
2293 path = os.path.join(self.upstream_root, project)
2294 repo = git.Repo.init(path)
2295
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002296 with repo.config_writer() as config_writer:
2297 config_writer.set_value('user', 'email', 'user@example.com')
2298 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002299
Clark Boylanb640e052014-04-03 16:41:46 -07002300 repo.index.commit('initial commit')
2301 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002302 if tag:
2303 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002304
James E. Blair97d902e2014-08-21 13:25:56 -07002305 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002306 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002307 repo.git.clean('-x', '-f', '-d')
2308
James E. Blair97d902e2014-08-21 13:25:56 -07002309 def create_branch(self, project, branch):
2310 path = os.path.join(self.upstream_root, project)
2311 repo = git.Repo.init(path)
2312 fn = os.path.join(path, 'README')
2313
2314 branch_head = repo.create_head(branch)
2315 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002316 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002317 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002318 f.close()
2319 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002320 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002321
James E. Blair97d902e2014-08-21 13:25:56 -07002322 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002323 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002324 repo.git.clean('-x', '-f', '-d')
2325
Sachi King9f16d522016-03-16 12:20:45 +11002326 def create_commit(self, project):
2327 path = os.path.join(self.upstream_root, project)
2328 repo = git.Repo(path)
2329 repo.head.reference = repo.heads['master']
2330 file_name = os.path.join(path, 'README')
2331 with open(file_name, 'a') as f:
2332 f.write('creating fake commit\n')
2333 repo.index.add([file_name])
2334 commit = repo.index.commit('Creating a fake commit')
2335 return commit.hexsha
2336
James E. Blairf4a5f022017-04-18 14:01:10 -07002337 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002338 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002339 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002340 while len(self.builds):
2341 self.release(self.builds[0])
2342 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002343 i += 1
2344 if count is not None and i >= count:
2345 break
James E. Blairb8c16472015-05-05 14:55:26 -07002346
James E. Blairdf25ddc2017-07-08 07:57:09 -07002347 def getSortedBuilds(self):
2348 "Return the list of currently running builds sorted by name"
2349
2350 return sorted(self.builds, key=lambda x: x.name)
2351
Clark Boylanb640e052014-04-03 16:41:46 -07002352 def release(self, job):
2353 if isinstance(job, FakeBuild):
2354 job.release()
2355 else:
2356 job.waiting = False
2357 self.log.debug("Queued job %s released" % job.unique)
2358 self.gearman_server.wakeConnections()
2359
2360 def getParameter(self, job, name):
2361 if isinstance(job, FakeBuild):
2362 return job.parameters[name]
2363 else:
2364 parameters = json.loads(job.arguments)
2365 return parameters[name]
2366
Clark Boylanb640e052014-04-03 16:41:46 -07002367 def haveAllBuildsReported(self):
2368 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002369 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002370 return False
2371 # Find out if every build that the worker has completed has been
2372 # reported back to Zuul. If it hasn't then that means a Gearman
2373 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002374 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002375 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002376 if not zbuild:
2377 # It has already been reported
2378 continue
2379 # It hasn't been reported yet.
2380 return False
2381 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002382 worker = self.executor_server.executor_worker
2383 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002384 if connection.state == 'GRAB_WAIT':
2385 return False
2386 return True
2387
2388 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002389 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002390 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002391 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002392 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002393 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002394 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002395 for j in conn.related_jobs.values():
2396 if j.unique == build.uuid:
2397 client_job = j
2398 break
2399 if not client_job:
2400 self.log.debug("%s is not known to the gearman client" %
2401 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002402 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002403 if not client_job.handle:
2404 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002405 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002406 server_job = self.gearman_server.jobs.get(client_job.handle)
2407 if not server_job:
2408 self.log.debug("%s is not known to the gearman server" %
2409 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002410 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002411 if not hasattr(server_job, 'waiting'):
2412 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002413 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002414 if server_job.waiting:
2415 continue
James E. Blair17302972016-08-10 16:11:42 -07002416 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002417 self.log.debug("%s has not reported start" % build)
2418 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002419 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002420 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002421 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002422 if worker_build:
2423 if worker_build.isWaiting():
2424 continue
2425 else:
2426 self.log.debug("%s is running" % worker_build)
2427 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002428 else:
James E. Blair962220f2016-08-03 11:22:38 -07002429 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002430 return False
James E. Blaira002b032017-04-18 10:35:48 -07002431 for (build_uuid, job_worker) in \
2432 self.executor_server.job_workers.items():
2433 if build_uuid not in seen_builds:
2434 self.log.debug("%s is not finalized" % build_uuid)
2435 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002436 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002437
James E. Blairdce6cea2016-12-20 16:45:32 -08002438 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002439 if self.fake_nodepool.paused:
2440 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002441 if self.sched.nodepool.requests:
2442 return False
2443 return True
2444
Jan Hruban6b71aff2015-10-22 16:58:08 +02002445 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002446 for event_queue in self.event_queues:
2447 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002448
2449 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002450 for event_queue in self.event_queues:
2451 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002452
Clark Boylanb640e052014-04-03 16:41:46 -07002453 def waitUntilSettled(self):
2454 self.log.debug("Waiting until settled...")
2455 start = time.time()
2456 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002457 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002458 self.log.error("Timeout waiting for Zuul to settle")
2459 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002460 for event_queue in self.event_queues:
2461 self.log.error(" %s: %s" %
2462 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002463 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002464 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002465 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002466 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002467 self.log.error("All requests completed: %s" %
2468 (self.areAllNodeRequestsComplete(),))
2469 self.log.error("Merge client jobs: %s" %
2470 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002471 raise Exception("Timeout waiting for Zuul to settle")
2472 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002473
Paul Belanger174a8272017-03-14 13:20:10 -04002474 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002475 # have all build states propogated to zuul?
2476 if self.haveAllBuildsReported():
2477 # Join ensures that the queue is empty _and_ events have been
2478 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002479 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002480 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002481 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002482 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002483 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002484 self.areAllNodeRequestsComplete() and
2485 all(self.eventQueuesEmpty())):
2486 # The queue empty check is placed at the end to
2487 # ensure that if a component adds an event between
2488 # when locked the run handler and checked that the
2489 # components were stable, we don't erroneously
2490 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002491 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002492 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002493 self.log.debug("...settled.")
2494 return
2495 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002496 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002497 self.sched.wake_event.wait(0.1)
2498
2499 def countJobResults(self, jobs, result):
2500 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002501 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002502
Monty Taylor0d926122017-05-24 08:07:56 -05002503 def getBuildByName(self, name):
2504 for build in self.builds:
2505 if build.name == name:
2506 return build
2507 raise Exception("Unable to find build %s" % name)
2508
James E. Blair96c6bf82016-01-15 16:20:40 -08002509 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002510 for job in self.history:
2511 if (job.name == name and
2512 (project is None or
2513 job.parameters['ZUUL_PROJECT'] == project)):
2514 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002515 raise Exception("Unable to find job %s in history" % name)
2516
2517 def assertEmptyQueues(self):
2518 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002519 for tenant in self.sched.abide.tenants.values():
2520 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002521 for pipeline_queue in pipeline.queues:
2522 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002523 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002524 pipeline.name, pipeline_queue.name,
2525 pipeline_queue.queue))
2526 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002527 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002528
2529 def assertReportedStat(self, key, value=None, kind=None):
2530 start = time.time()
2531 while time.time() < (start + 5):
2532 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002533 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002534 if key == k:
2535 if value is None and kind is None:
2536 return
2537 elif value:
2538 if value == v:
2539 return
2540 elif kind:
2541 if v.endswith('|' + kind):
2542 return
2543 time.sleep(0.1)
2544
Clark Boylanb640e052014-04-03 16:41:46 -07002545 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002546
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002547 def assertBuilds(self, builds):
2548 """Assert that the running builds are as described.
2549
2550 The list of running builds is examined and must match exactly
2551 the list of builds described by the input.
2552
2553 :arg list builds: A list of dictionaries. Each item in the
2554 list must match the corresponding build in the build
2555 history, and each element of the dictionary must match the
2556 corresponding attribute of the build.
2557
2558 """
James E. Blair3158e282016-08-19 09:34:11 -07002559 try:
2560 self.assertEqual(len(self.builds), len(builds))
2561 for i, d in enumerate(builds):
2562 for k, v in d.items():
2563 self.assertEqual(
2564 getattr(self.builds[i], k), v,
2565 "Element %i in builds does not match" % (i,))
2566 except Exception:
2567 for build in self.builds:
2568 self.log.error("Running build: %s" % build)
2569 else:
2570 self.log.error("No running builds")
2571 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002572
James E. Blairb536ecc2016-08-31 10:11:42 -07002573 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002574 """Assert that the completed builds are as described.
2575
2576 The list of completed builds is examined and must match
2577 exactly the list of builds described by the input.
2578
2579 :arg list history: A list of dictionaries. Each item in the
2580 list must match the corresponding build in the build
2581 history, and each element of the dictionary must match the
2582 corresponding attribute of the build.
2583
James E. Blairb536ecc2016-08-31 10:11:42 -07002584 :arg bool ordered: If true, the history must match the order
2585 supplied, if false, the builds are permitted to have
2586 arrived in any order.
2587
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002588 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002589 def matches(history_item, item):
2590 for k, v in item.items():
2591 if getattr(history_item, k) != v:
2592 return False
2593 return True
James E. Blair3158e282016-08-19 09:34:11 -07002594 try:
2595 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002596 if ordered:
2597 for i, d in enumerate(history):
2598 if not matches(self.history[i], d):
2599 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002600 "Element %i in history does not match %s" %
2601 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002602 else:
2603 unseen = self.history[:]
2604 for i, d in enumerate(history):
2605 found = False
2606 for unseen_item in unseen:
2607 if matches(unseen_item, d):
2608 found = True
2609 unseen.remove(unseen_item)
2610 break
2611 if not found:
2612 raise Exception("No match found for element %i "
2613 "in history" % (i,))
2614 if unseen:
2615 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002616 except Exception:
2617 for build in self.history:
2618 self.log.error("Completed build: %s" % build)
2619 else:
2620 self.log.error("No completed builds")
2621 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002622
James E. Blair6ac368c2016-12-22 18:07:20 -08002623 def printHistory(self):
2624 """Log the build history.
2625
2626 This can be useful during tests to summarize what jobs have
2627 completed.
2628
2629 """
2630 self.log.debug("Build history:")
2631 for build in self.history:
2632 self.log.debug(build)
2633
James E. Blair59fdbac2015-12-07 17:08:06 -08002634 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002635 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2636
James E. Blair9ea70072017-04-19 16:05:30 -07002637 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002638 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002639 if not os.path.exists(root):
2640 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002641 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2642 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002643- tenant:
2644 name: openstack
2645 source:
2646 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002647 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002648 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002649 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002650 - org/project
2651 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002652 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002653 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002654 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002655 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002656 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002657
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002658 def addCommitToRepo(self, project, message, files,
2659 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002660 path = os.path.join(self.upstream_root, project)
2661 repo = git.Repo(path)
2662 repo.head.reference = branch
2663 zuul.merger.merger.reset_repo_to_head(repo)
2664 for fn, content in files.items():
2665 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002666 try:
2667 os.makedirs(os.path.dirname(fn))
2668 except OSError:
2669 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002670 with open(fn, 'w') as f:
2671 f.write(content)
2672 repo.index.add([fn])
2673 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002674 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002675 repo.heads[branch].commit = commit
2676 repo.head.reference = branch
2677 repo.git.clean('-x', '-f', '-d')
2678 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002679 if tag:
2680 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002681 return before
2682
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002683 def commitConfigUpdate(self, project_name, source_name):
2684 """Commit an update to zuul.yaml
2685
2686 This overwrites the zuul.yaml in the specificed project with
2687 the contents specified.
2688
2689 :arg str project_name: The name of the project containing
2690 zuul.yaml (e.g., common-config)
2691
2692 :arg str source_name: The path to the file (underneath the
2693 test fixture directory) whose contents should be used to
2694 replace zuul.yaml.
2695 """
2696
2697 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002698 files = {}
2699 with open(source_path, 'r') as f:
2700 data = f.read()
2701 layout = yaml.safe_load(data)
2702 files['zuul.yaml'] = data
2703 for item in layout:
2704 if 'job' in item:
2705 jobname = item['job']['name']
2706 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002707 before = self.addCommitToRepo(
2708 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002709 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002710 return before
2711
James E. Blair7fc8daa2016-08-08 15:37:15 -07002712 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002713
James E. Blair7fc8daa2016-08-08 15:37:15 -07002714 """Inject a Fake (Gerrit) event.
2715
2716 This method accepts a JSON-encoded event and simulates Zuul
2717 having received it from Gerrit. It could (and should)
2718 eventually apply to any connection type, but is currently only
2719 used with Gerrit connections. The name of the connection is
2720 used to look up the corresponding server, and the event is
2721 simulated as having been received by all Zuul connections
2722 attached to that server. So if two Gerrit connections in Zuul
2723 are connected to the same Gerrit server, and you invoke this
2724 method specifying the name of one of them, the event will be
2725 received by both.
2726
2727 .. note::
2728
2729 "self.fake_gerrit.addEvent" calls should be migrated to
2730 this method.
2731
2732 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002733 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002734 :arg str event: The JSON-encoded event.
2735
2736 """
2737 specified_conn = self.connections.connections[connection]
2738 for conn in self.connections.connections.values():
2739 if (isinstance(conn, specified_conn.__class__) and
2740 specified_conn.server == conn.server):
2741 conn.addEvent(event)
2742
James E. Blaird8af5422017-05-24 13:59:40 -07002743 def getUpstreamRepos(self, projects):
2744 """Return upstream git repo objects for the listed projects
2745
2746 :arg list projects: A list of strings, each the canonical name
2747 of a project.
2748
2749 :returns: A dictionary of {name: repo} for every listed
2750 project.
2751 :rtype: dict
2752
2753 """
2754
2755 repos = {}
2756 for project in projects:
2757 # FIXME(jeblair): the upstream root does not yet have a
2758 # hostname component; that needs to be added, and this
2759 # line removed:
2760 tmp_project_name = '/'.join(project.split('/')[1:])
2761 path = os.path.join(self.upstream_root, tmp_project_name)
2762 repo = git.Repo(path)
2763 repos[project] = repo
2764 return repos
2765
James E. Blair3f876d52016-07-22 13:07:14 -07002766
2767class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002768 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002769 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002770
Joshua Heskethd78b4482015-09-14 16:56:34 -06002771
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002772class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002773 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002774 use_ssl = True
2775
2776
Joshua Heskethd78b4482015-09-14 16:56:34 -06002777class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002778 def setup_config(self):
2779 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002780 for section_name in self.config.sections():
2781 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2782 section_name, re.I)
2783 if not con_match:
2784 continue
2785
2786 if self.config.get(section_name, 'driver') == 'sql':
2787 f = MySQLSchemaFixture()
2788 self.useFixture(f)
2789 if (self.config.get(section_name, 'dburi') ==
2790 '$MYSQL_FIXTURE_DBURI$'):
2791 self.config.set(section_name, 'dburi', f.dburi)