blob: 2b6147af19edaae120be1c536f863ed20a960e33 [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
James E. Blair6193a1f2017-07-21 15:13:15 -07001134 items = self.parameters['zuul']['items']
1135 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1136 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001137
James E. Blair3158e282016-08-19 09:34:11 -07001138 def __repr__(self):
1139 waiting = ''
1140 if self.waiting:
1141 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001142 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1143 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001144
Clark Boylanb640e052014-04-03 16:41:46 -07001145 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001146 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001147 self.wait_condition.acquire()
1148 self.wait_condition.notify()
1149 self.waiting = False
1150 self.log.debug("Build %s released" % self.unique)
1151 self.wait_condition.release()
1152
1153 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001154 """Return whether this build is being held.
1155
1156 :returns: Whether the build is being held.
1157 :rtype: bool
1158 """
1159
Clark Boylanb640e052014-04-03 16:41:46 -07001160 self.wait_condition.acquire()
1161 if self.waiting:
1162 ret = True
1163 else:
1164 ret = False
1165 self.wait_condition.release()
1166 return ret
1167
1168 def _wait(self):
1169 self.wait_condition.acquire()
1170 self.waiting = True
1171 self.log.debug("Build %s waiting" % self.unique)
1172 self.wait_condition.wait()
1173 self.wait_condition.release()
1174
1175 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001176 self.log.debug('Running build %s' % self.unique)
1177
Paul Belanger174a8272017-03-14 13:20:10 -04001178 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001179 self.log.debug('Holding build %s' % self.unique)
1180 self._wait()
1181 self.log.debug("Build %s continuing" % self.unique)
1182
James E. Blair412fba82017-01-26 15:00:50 -08001183 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001184 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001185 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001186 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001187 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001188 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001189 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001190
James E. Blaire1767bc2016-08-02 10:00:27 -07001191 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001192
James E. Blaira5dba232016-08-08 15:53:24 -07001193 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001194 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001195 for change in changes:
1196 if self.hasChanges(change):
1197 return True
1198 return False
1199
James E. Blaire7b99a02016-08-05 14:27:34 -07001200 def hasChanges(self, *changes):
1201 """Return whether this build has certain changes in its git repos.
1202
1203 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001204 are expected to be present (in order) in the git repository of
1205 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001206
1207 :returns: Whether the build has the indicated changes.
1208 :rtype: bool
1209
1210 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001211 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001212 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001213 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001214 try:
1215 repo = git.Repo(path)
1216 except NoSuchPathError as e:
1217 self.log.debug('%s' % e)
1218 return False
James E. Blair247cab72017-07-20 16:52:36 -07001219 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001220 commit_message = '%s-1' % change.subject
1221 self.log.debug("Checking if build %s has changes; commit_message "
1222 "%s; repo_messages %s" % (self, commit_message,
1223 repo_messages))
1224 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001225 self.log.debug(" messages do not match")
1226 return False
1227 self.log.debug(" OK")
1228 return True
1229
James E. Blaird8af5422017-05-24 13:59:40 -07001230 def getWorkspaceRepos(self, projects):
1231 """Return workspace git repo objects for the listed projects
1232
1233 :arg list projects: A list of strings, each the canonical name
1234 of a project.
1235
1236 :returns: A dictionary of {name: repo} for every listed
1237 project.
1238 :rtype: dict
1239
1240 """
1241
1242 repos = {}
1243 for project in projects:
1244 path = os.path.join(self.jobdir.src_root, project)
1245 repo = git.Repo(path)
1246 repos[project] = repo
1247 return repos
1248
Clark Boylanb640e052014-04-03 16:41:46 -07001249
Paul Belanger174a8272017-03-14 13:20:10 -04001250class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1251 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001252
Paul Belanger174a8272017-03-14 13:20:10 -04001253 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001254 they will report that they have started but then pause until
1255 released before reporting completion. This attribute may be
1256 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001257 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001258 be explicitly released.
1259
1260 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001261 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001262 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001263 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001264 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001265 self.hold_jobs_in_build = False
1266 self.lock = threading.Lock()
1267 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001268 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001269 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001270 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001271
James E. Blaira5dba232016-08-08 15:53:24 -07001272 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001273 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001274
1275 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001276 :arg Change change: The :py:class:`~tests.base.FakeChange`
1277 instance which should cause the job to fail. This job
1278 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001279
1280 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001281 l = self.fail_tests.get(name, [])
1282 l.append(change)
1283 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001284
James E. Blair962220f2016-08-03 11:22:38 -07001285 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001286 """Release a held build.
1287
1288 :arg str regex: A regular expression which, if supplied, will
1289 cause only builds with matching names to be released. If
1290 not supplied, all builds will be released.
1291
1292 """
James E. Blair962220f2016-08-03 11:22:38 -07001293 builds = self.running_builds[:]
1294 self.log.debug("Releasing build %s (%s)" % (regex,
1295 len(self.running_builds)))
1296 for build in builds:
1297 if not regex or re.match(regex, build.name):
1298 self.log.debug("Releasing build %s" %
1299 (build.parameters['ZUUL_UUID']))
1300 build.release()
1301 else:
1302 self.log.debug("Not releasing build %s" %
1303 (build.parameters['ZUUL_UUID']))
1304 self.log.debug("Done releasing builds %s (%s)" %
1305 (regex, len(self.running_builds)))
1306
Paul Belanger174a8272017-03-14 13:20:10 -04001307 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001308 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001309 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001311 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001312 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001313 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001314 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001315 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1316 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001317
1318 def stopJob(self, job):
1319 self.log.debug("handle stop")
1320 parameters = json.loads(job.arguments)
1321 uuid = parameters['uuid']
1322 for build in self.running_builds:
1323 if build.unique == uuid:
1324 build.aborted = True
1325 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001326 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001327
James E. Blaira002b032017-04-18 10:35:48 -07001328 def stop(self):
1329 for build in self.running_builds:
1330 build.release()
1331 super(RecordingExecutorServer, self).stop()
1332
Joshua Hesketh50c21782016-10-13 21:34:14 +11001333
Paul Belanger174a8272017-03-14 13:20:10 -04001334class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001335 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001336 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001337 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001338 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001339 if not commit: # merge conflict
1340 self.recordResult('MERGER_FAILURE')
1341 return commit
1342
1343 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001344 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001345 self.executor_server.lock.acquire()
1346 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001347 BuildHistory(name=build.name, result=result, changes=build.changes,
1348 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001349 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001350 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001351 pipeline=build.parameters['ZUUL_PIPELINE'])
1352 )
Paul Belanger174a8272017-03-14 13:20:10 -04001353 self.executor_server.running_builds.remove(build)
1354 del self.executor_server.job_builds[self.job.unique]
1355 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001356
1357 def runPlaybooks(self, args):
1358 build = self.executor_server.job_builds[self.job.unique]
1359 build.jobdir = self.jobdir
1360
1361 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1362 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001363 return result
1364
James E. Blair74a82cf2017-07-12 17:23:08 -07001365 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001366 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001367
Paul Belanger174a8272017-03-14 13:20:10 -04001368 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001369 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001370 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001371 else:
1372 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001373 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001374
James E. Blairad8dca02017-02-21 11:48:32 -05001375 def getHostList(self, args):
1376 self.log.debug("hostlist")
1377 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001378 for host in hosts:
1379 host['host_vars']['ansible_connection'] = 'local'
1380
1381 hosts.append(dict(
1382 name='localhost',
1383 host_vars=dict(ansible_connection='local'),
1384 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001385 return hosts
1386
James E. Blairf5dbd002015-12-23 15:26:17 -08001387
Clark Boylanb640e052014-04-03 16:41:46 -07001388class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001389 """A Gearman server for use in tests.
1390
1391 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1392 added to the queue but will not be distributed to workers
1393 until released. This attribute may be changed at any time and
1394 will take effect for subsequently enqueued jobs, but
1395 previously held jobs will still need to be explicitly
1396 released.
1397
1398 """
1399
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001400 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001402 if use_ssl:
1403 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1404 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1405 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1406 else:
1407 ssl_ca = None
1408 ssl_cert = None
1409 ssl_key = None
1410
1411 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1412 ssl_cert=ssl_cert,
1413 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001414
1415 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001416 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1417 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001418 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001419 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001420 job.waiting = self.hold_jobs_in_queue
1421 else:
1422 job.waiting = False
1423 if job.waiting:
1424 continue
1425 if job.name in connection.functions:
1426 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001427 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001428 connection.related_jobs[job.handle] = job
1429 job.worker_connection = connection
1430 job.running = True
1431 return job
1432 return None
1433
1434 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001435 """Release a held job.
1436
1437 :arg str regex: A regular expression which, if supplied, will
1438 cause only jobs with matching names to be released. If
1439 not supplied, all jobs will be released.
1440 """
Clark Boylanb640e052014-04-03 16:41:46 -07001441 released = False
1442 qlen = (len(self.high_queue) + len(self.normal_queue) +
1443 len(self.low_queue))
1444 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1445 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001446 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001447 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001448 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001449 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001450 self.log.debug("releasing queued job %s" %
1451 job.unique)
1452 job.waiting = False
1453 released = True
1454 else:
1455 self.log.debug("not releasing queued job %s" %
1456 job.unique)
1457 if released:
1458 self.wakeConnections()
1459 qlen = (len(self.high_queue) + len(self.normal_queue) +
1460 len(self.low_queue))
1461 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1462
1463
1464class FakeSMTP(object):
1465 log = logging.getLogger('zuul.FakeSMTP')
1466
1467 def __init__(self, messages, server, port):
1468 self.server = server
1469 self.port = port
1470 self.messages = messages
1471
1472 def sendmail(self, from_email, to_email, msg):
1473 self.log.info("Sending email from %s, to %s, with msg %s" % (
1474 from_email, to_email, msg))
1475
1476 headers = msg.split('\n\n', 1)[0]
1477 body = msg.split('\n\n', 1)[1]
1478
1479 self.messages.append(dict(
1480 from_email=from_email,
1481 to_email=to_email,
1482 msg=msg,
1483 headers=headers,
1484 body=body,
1485 ))
1486
1487 return True
1488
1489 def quit(self):
1490 return True
1491
1492
James E. Blairdce6cea2016-12-20 16:45:32 -08001493class FakeNodepool(object):
1494 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001495 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001496
1497 log = logging.getLogger("zuul.test.FakeNodepool")
1498
1499 def __init__(self, host, port, chroot):
1500 self.client = kazoo.client.KazooClient(
1501 hosts='%s:%s%s' % (host, port, chroot))
1502 self.client.start()
1503 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001504 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001505 self.thread = threading.Thread(target=self.run)
1506 self.thread.daemon = True
1507 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001508 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001509
1510 def stop(self):
1511 self._running = False
1512 self.thread.join()
1513 self.client.stop()
1514 self.client.close()
1515
1516 def run(self):
1517 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001518 try:
1519 self._run()
1520 except Exception:
1521 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001522 time.sleep(0.1)
1523
1524 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001525 if self.paused:
1526 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001527 for req in self.getNodeRequests():
1528 self.fulfillRequest(req)
1529
1530 def getNodeRequests(self):
1531 try:
1532 reqids = self.client.get_children(self.REQUEST_ROOT)
1533 except kazoo.exceptions.NoNodeError:
1534 return []
1535 reqs = []
1536 for oid in sorted(reqids):
1537 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001538 try:
1539 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001540 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001541 data['_oid'] = oid
1542 reqs.append(data)
1543 except kazoo.exceptions.NoNodeError:
1544 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001545 return reqs
1546
James E. Blaire18d4602017-01-05 11:17:28 -08001547 def getNodes(self):
1548 try:
1549 nodeids = self.client.get_children(self.NODE_ROOT)
1550 except kazoo.exceptions.NoNodeError:
1551 return []
1552 nodes = []
1553 for oid in sorted(nodeids):
1554 path = self.NODE_ROOT + '/' + oid
1555 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001556 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001557 data['_oid'] = oid
1558 try:
1559 lockfiles = self.client.get_children(path + '/lock')
1560 except kazoo.exceptions.NoNodeError:
1561 lockfiles = []
1562 if lockfiles:
1563 data['_lock'] = True
1564 else:
1565 data['_lock'] = False
1566 nodes.append(data)
1567 return nodes
1568
James E. Blaira38c28e2017-01-04 10:33:20 -08001569 def makeNode(self, request_id, node_type):
1570 now = time.time()
1571 path = '/nodepool/nodes/'
1572 data = dict(type=node_type,
1573 provider='test-provider',
1574 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001575 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001576 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001577 public_ipv4='127.0.0.1',
1578 private_ipv4=None,
1579 public_ipv6=None,
1580 allocated_to=request_id,
1581 state='ready',
1582 state_time=now,
1583 created_time=now,
1584 updated_time=now,
1585 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001586 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001587 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001588 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001589 path = self.client.create(path, data,
1590 makepath=True,
1591 sequence=True)
1592 nodeid = path.split("/")[-1]
1593 return nodeid
1594
James E. Blair6ab79e02017-01-06 10:10:17 -08001595 def addFailRequest(self, request):
1596 self.fail_requests.add(request['_oid'])
1597
James E. Blairdce6cea2016-12-20 16:45:32 -08001598 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001599 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001600 return
1601 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001602 oid = request['_oid']
1603 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001604
James E. Blair6ab79e02017-01-06 10:10:17 -08001605 if oid in self.fail_requests:
1606 request['state'] = 'failed'
1607 else:
1608 request['state'] = 'fulfilled'
1609 nodes = []
1610 for node in request['node_types']:
1611 nodeid = self.makeNode(oid, node)
1612 nodes.append(nodeid)
1613 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001614
James E. Blaira38c28e2017-01-04 10:33:20 -08001615 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001616 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001617 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001618 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001619 try:
1620 self.client.set(path, data)
1621 except kazoo.exceptions.NoNodeError:
1622 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001623
1624
James E. Blair498059b2016-12-20 13:50:13 -08001625class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001626 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001627 super(ChrootedKazooFixture, self).__init__()
1628
1629 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1630 if ':' in zk_host:
1631 host, port = zk_host.split(':')
1632 else:
1633 host = zk_host
1634 port = None
1635
1636 self.zookeeper_host = host
1637
1638 if not port:
1639 self.zookeeper_port = 2181
1640 else:
1641 self.zookeeper_port = int(port)
1642
Clark Boylan621ec9a2017-04-07 17:41:33 -07001643 self.test_id = test_id
1644
James E. Blair498059b2016-12-20 13:50:13 -08001645 def _setUp(self):
1646 # Make sure the test chroot paths do not conflict
1647 random_bits = ''.join(random.choice(string.ascii_lowercase +
1648 string.ascii_uppercase)
1649 for x in range(8))
1650
Clark Boylan621ec9a2017-04-07 17:41:33 -07001651 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001652 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1653
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001654 self.addCleanup(self._cleanup)
1655
James E. Blair498059b2016-12-20 13:50:13 -08001656 # Ensure the chroot path exists and clean up any pre-existing znodes.
1657 _tmp_client = kazoo.client.KazooClient(
1658 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1659 _tmp_client.start()
1660
1661 if _tmp_client.exists(self.zookeeper_chroot):
1662 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1663
1664 _tmp_client.ensure_path(self.zookeeper_chroot)
1665 _tmp_client.stop()
1666 _tmp_client.close()
1667
James E. Blair498059b2016-12-20 13:50:13 -08001668 def _cleanup(self):
1669 '''Remove the chroot path.'''
1670 # Need a non-chroot'ed client to remove the chroot path
1671 _tmp_client = kazoo.client.KazooClient(
1672 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1673 _tmp_client.start()
1674 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1675 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001676 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001677
1678
Joshua Heskethd78b4482015-09-14 16:56:34 -06001679class MySQLSchemaFixture(fixtures.Fixture):
1680 def setUp(self):
1681 super(MySQLSchemaFixture, self).setUp()
1682
1683 random_bits = ''.join(random.choice(string.ascii_lowercase +
1684 string.ascii_uppercase)
1685 for x in range(8))
1686 self.name = '%s_%s' % (random_bits, os.getpid())
1687 self.passwd = uuid.uuid4().hex
1688 db = pymysql.connect(host="localhost",
1689 user="openstack_citest",
1690 passwd="openstack_citest",
1691 db="openstack_citest")
1692 cur = db.cursor()
1693 cur.execute("create database %s" % self.name)
1694 cur.execute(
1695 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1696 (self.name, self.name, self.passwd))
1697 cur.execute("flush privileges")
1698
1699 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1700 self.passwd,
1701 self.name)
1702 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1703 self.addCleanup(self.cleanup)
1704
1705 def cleanup(self):
1706 db = pymysql.connect(host="localhost",
1707 user="openstack_citest",
1708 passwd="openstack_citest",
1709 db="openstack_citest")
1710 cur = db.cursor()
1711 cur.execute("drop database %s" % self.name)
1712 cur.execute("drop user '%s'@'localhost'" % self.name)
1713 cur.execute("flush privileges")
1714
1715
Maru Newby3fe5f852015-01-13 04:22:14 +00001716class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001717 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001718 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001719
James E. Blair1c236df2017-02-01 14:07:24 -08001720 def attachLogs(self, *args):
1721 def reader():
1722 self._log_stream.seek(0)
1723 while True:
1724 x = self._log_stream.read(4096)
1725 if not x:
1726 break
1727 yield x.encode('utf8')
1728 content = testtools.content.content_from_reader(
1729 reader,
1730 testtools.content_type.UTF8_TEXT,
1731 False)
1732 self.addDetail('logging', content)
1733
Clark Boylanb640e052014-04-03 16:41:46 -07001734 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001735 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001736 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1737 try:
1738 test_timeout = int(test_timeout)
1739 except ValueError:
1740 # If timeout value is invalid do not set a timeout.
1741 test_timeout = 0
1742 if test_timeout > 0:
1743 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1744
1745 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1746 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1747 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1748 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1749 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1750 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1751 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1752 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1753 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1754 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001755 self._log_stream = StringIO()
1756 self.addOnException(self.attachLogs)
1757 else:
1758 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001759
James E. Blair73b41772017-05-22 13:22:55 -07001760 # NOTE(jeblair): this is temporary extra debugging to try to
1761 # track down a possible leak.
1762 orig_git_repo_init = git.Repo.__init__
1763
1764 def git_repo_init(myself, *args, **kw):
1765 orig_git_repo_init(myself, *args, **kw)
1766 self.log.debug("Created git repo 0x%x %s" %
1767 (id(myself), repr(myself)))
1768
1769 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1770 git_repo_init))
1771
James E. Blair1c236df2017-02-01 14:07:24 -08001772 handler = logging.StreamHandler(self._log_stream)
1773 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1774 '%(levelname)-8s %(message)s')
1775 handler.setFormatter(formatter)
1776
1777 logger = logging.getLogger()
1778 logger.setLevel(logging.DEBUG)
1779 logger.addHandler(handler)
1780
Clark Boylan3410d532017-04-25 12:35:29 -07001781 # Make sure we don't carry old handlers around in process state
1782 # which slows down test runs
1783 self.addCleanup(logger.removeHandler, handler)
1784 self.addCleanup(handler.close)
1785 self.addCleanup(handler.flush)
1786
James E. Blair1c236df2017-02-01 14:07:24 -08001787 # NOTE(notmorgan): Extract logging overrides for specific
1788 # libraries from the OS_LOG_DEFAULTS env and create loggers
1789 # for each. This is used to limit the output during test runs
1790 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001791 log_defaults_from_env = os.environ.get(
1792 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001793 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001794
James E. Blairdce6cea2016-12-20 16:45:32 -08001795 if log_defaults_from_env:
1796 for default in log_defaults_from_env.split(','):
1797 try:
1798 name, level_str = default.split('=', 1)
1799 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001800 logger = logging.getLogger(name)
1801 logger.setLevel(level)
1802 logger.addHandler(handler)
1803 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001804 except ValueError:
1805 # NOTE(notmorgan): Invalid format of the log default,
1806 # skip and don't try and apply a logger for the
1807 # specified module
1808 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001809
Maru Newby3fe5f852015-01-13 04:22:14 +00001810
1811class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001812 """A test case with a functioning Zuul.
1813
1814 The following class variables are used during test setup and can
1815 be overidden by subclasses but are effectively read-only once a
1816 test method starts running:
1817
1818 :cvar str config_file: This points to the main zuul config file
1819 within the fixtures directory. Subclasses may override this
1820 to obtain a different behavior.
1821
1822 :cvar str tenant_config_file: This is the tenant config file
1823 (which specifies from what git repos the configuration should
1824 be loaded). It defaults to the value specified in
1825 `config_file` but can be overidden by subclasses to obtain a
1826 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001827 configuration. See also the :py:func:`simple_layout`
1828 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001829
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001830 :cvar bool create_project_keys: Indicates whether Zuul should
1831 auto-generate keys for each project, or whether the test
1832 infrastructure should insert dummy keys to save time during
1833 startup. Defaults to False.
1834
James E. Blaire7b99a02016-08-05 14:27:34 -07001835 The following are instance variables that are useful within test
1836 methods:
1837
1838 :ivar FakeGerritConnection fake_<connection>:
1839 A :py:class:`~tests.base.FakeGerritConnection` will be
1840 instantiated for each connection present in the config file
1841 and stored here. For instance, `fake_gerrit` will hold the
1842 FakeGerritConnection object for a connection named `gerrit`.
1843
1844 :ivar FakeGearmanServer gearman_server: An instance of
1845 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1846 server that all of the Zuul components in this test use to
1847 communicate with each other.
1848
Paul Belanger174a8272017-03-14 13:20:10 -04001849 :ivar RecordingExecutorServer executor_server: An instance of
1850 :py:class:`~tests.base.RecordingExecutorServer` which is the
1851 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001852
1853 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1854 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001855 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001856 list upon completion.
1857
1858 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1859 objects representing completed builds. They are appended to
1860 the list in the order they complete.
1861
1862 """
1863
James E. Blair83005782015-12-11 14:46:03 -08001864 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001865 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001866 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001867 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001868
1869 def _startMerger(self):
1870 self.merge_server = zuul.merger.server.MergeServer(self.config,
1871 self.connections)
1872 self.merge_server.start()
1873
Maru Newby3fe5f852015-01-13 04:22:14 +00001874 def setUp(self):
1875 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001876
1877 self.setupZK()
1878
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001879 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001880 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001881 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1882 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001883 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001884 tmp_root = tempfile.mkdtemp(
1885 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001886 self.test_root = os.path.join(tmp_root, "zuul-test")
1887 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001888 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001889 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001890 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001891 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1892 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001893
1894 if os.path.exists(self.test_root):
1895 shutil.rmtree(self.test_root)
1896 os.makedirs(self.test_root)
1897 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001898 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001899 os.makedirs(self.merger_state_root)
1900 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001901
1902 # Make per test copy of Configuration.
1903 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001904 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1905 if not os.path.exists(self.private_key_file):
1906 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1907 shutil.copy(src_private_key_file, self.private_key_file)
1908 shutil.copy('{}.pub'.format(src_private_key_file),
1909 '{}.pub'.format(self.private_key_file))
1910 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001911 self.config.set('scheduler', 'tenant_config',
1912 os.path.join(
1913 FIXTURE_DIR,
1914 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001915 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001916 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001917 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001918 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001919 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001920 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001921
Clark Boylanb640e052014-04-03 16:41:46 -07001922 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001923 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1924 # see: https://github.com/jsocol/pystatsd/issues/61
1925 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001926 os.environ['STATSD_PORT'] = str(self.statsd.port)
1927 self.statsd.start()
1928 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001929 importlib.reload(statsd)
1930 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001931
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001932 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001933
1934 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001935 self.log.info("Gearman server on port %s" %
1936 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001937 if self.use_ssl:
1938 self.log.info('SSL enabled for gearman')
1939 self.config.set(
1940 'gearman', 'ssl_ca',
1941 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1942 self.config.set(
1943 'gearman', 'ssl_cert',
1944 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1945 self.config.set(
1946 'gearman', 'ssl_key',
1947 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001948
James E. Blaire511d2f2016-12-08 15:22:26 -08001949 gerritsource.GerritSource.replication_timeout = 1.5
1950 gerritsource.GerritSource.replication_retry_interval = 0.5
1951 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001952
Joshua Hesketh352264b2015-08-11 23:42:08 +10001953 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001954
Jan Hruban7083edd2015-08-21 14:00:54 +02001955 self.webapp = zuul.webapp.WebApp(
1956 self.sched, port=0, listen_address='127.0.0.1')
1957
Jan Hruban6b71aff2015-10-22 16:58:08 +02001958 self.event_queues = [
1959 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001960 self.sched.trigger_event_queue,
1961 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001962 ]
1963
James E. Blairfef78942016-03-11 16:28:56 -08001964 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001965 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001966
Paul Belanger174a8272017-03-14 13:20:10 -04001967 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001968 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001969 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001970 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001971 _test_root=self.test_root,
1972 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001973 self.executor_server.start()
1974 self.history = self.executor_server.build_history
1975 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001976
Paul Belanger174a8272017-03-14 13:20:10 -04001977 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001978 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001979 self.merge_client = zuul.merger.client.MergeClient(
1980 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001981 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001982 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001983 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001984
James E. Blair0d5a36e2017-02-21 10:53:44 -05001985 self.fake_nodepool = FakeNodepool(
1986 self.zk_chroot_fixture.zookeeper_host,
1987 self.zk_chroot_fixture.zookeeper_port,
1988 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001989
Paul Belanger174a8272017-03-14 13:20:10 -04001990 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001991 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001992 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001993 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001994
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001995 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001996
1997 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001998 self.webapp.start()
1999 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002000 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002001 # Cleanups are run in reverse order
2002 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002003 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002004 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002005
James E. Blairb9c0d772017-03-03 14:34:49 -08002006 self.sched.reconfigure(self.config)
2007 self.sched.resume()
2008
Tobias Henkel7df274b2017-05-26 17:41:11 +02002009 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002010 # Set up gerrit related fakes
2011 # Set a changes database so multiple FakeGerrit's can report back to
2012 # a virtual canonical database given by the configured hostname
2013 self.gerrit_changes_dbs = {}
2014
2015 def getGerritConnection(driver, name, config):
2016 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2017 con = FakeGerritConnection(driver, name, config,
2018 changes_db=db,
2019 upstream_root=self.upstream_root)
2020 self.event_queues.append(con.event_queue)
2021 setattr(self, 'fake_' + name, con)
2022 return con
2023
2024 self.useFixture(fixtures.MonkeyPatch(
2025 'zuul.driver.gerrit.GerritDriver.getConnection',
2026 getGerritConnection))
2027
Gregory Haynes4fc12542015-04-22 20:38:06 -07002028 def getGithubConnection(driver, name, config):
2029 con = FakeGithubConnection(driver, name, config,
2030 upstream_root=self.upstream_root)
2031 setattr(self, 'fake_' + name, con)
2032 return con
2033
2034 self.useFixture(fixtures.MonkeyPatch(
2035 'zuul.driver.github.GithubDriver.getConnection',
2036 getGithubConnection))
2037
James E. Blaire511d2f2016-12-08 15:22:26 -08002038 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002039 # TODO(jhesketh): This should come from lib.connections for better
2040 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002041 # Register connections from the config
2042 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002043
Joshua Hesketh352264b2015-08-11 23:42:08 +10002044 def FakeSMTPFactory(*args, **kw):
2045 args = [self.smtp_messages] + list(args)
2046 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002047
Joshua Hesketh352264b2015-08-11 23:42:08 +10002048 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002049
James E. Blaire511d2f2016-12-08 15:22:26 -08002050 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002051 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002052 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002053
James E. Blair83005782015-12-11 14:46:03 -08002054 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002055 # This creates the per-test configuration object. It can be
2056 # overriden by subclasses, but should not need to be since it
2057 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002058 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002059 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002060
James E. Blair39840362017-06-23 20:34:02 +01002061 sections = ['zuul', 'scheduler', 'executor', 'merger']
2062 for section in sections:
2063 if not self.config.has_section(section):
2064 self.config.add_section(section)
2065
James E. Blair06cc3922017-04-19 10:08:10 -07002066 if not self.setupSimpleLayout():
2067 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002068 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002069 self.tenant_config_file)
2070 git_path = os.path.join(
2071 os.path.dirname(
2072 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2073 'git')
2074 if os.path.exists(git_path):
2075 for reponame in os.listdir(git_path):
2076 project = reponame.replace('_', '/')
2077 self.copyDirToRepo(project,
2078 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002079 # Make test_root persist after ansible run for .flag test
2080 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002081 self.setupAllProjectKeys()
2082
James E. Blair06cc3922017-04-19 10:08:10 -07002083 def setupSimpleLayout(self):
2084 # If the test method has been decorated with a simple_layout,
2085 # use that instead of the class tenant_config_file. Set up a
2086 # single config-project with the specified layout, and
2087 # initialize repos for all of the 'project' entries which
2088 # appear in the layout.
2089 test_name = self.id().split('.')[-1]
2090 test = getattr(self, test_name)
2091 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002092 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002093 else:
2094 return False
2095
James E. Blairb70e55a2017-04-19 12:57:02 -07002096 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002097 path = os.path.join(FIXTURE_DIR, path)
2098 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002099 data = f.read()
2100 layout = yaml.safe_load(data)
2101 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002102 untrusted_projects = []
2103 for item in layout:
2104 if 'project' in item:
2105 name = item['project']['name']
2106 untrusted_projects.append(name)
2107 self.init_repo(name)
2108 self.addCommitToRepo(name, 'initial commit',
2109 files={'README': ''},
2110 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002111 if 'job' in item:
2112 jobname = item['job']['name']
2113 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002114
2115 root = os.path.join(self.test_root, "config")
2116 if not os.path.exists(root):
2117 os.makedirs(root)
2118 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2119 config = [{'tenant':
2120 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002121 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002122 {'config-projects': ['common-config'],
2123 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002124 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002125 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002126 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002127 os.path.join(FIXTURE_DIR, f.name))
2128
2129 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002130 self.addCommitToRepo('common-config', 'add content from fixture',
2131 files, branch='master', tag='init')
2132
2133 return True
2134
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002135 def setupAllProjectKeys(self):
2136 if self.create_project_keys:
2137 return
2138
James E. Blair39840362017-06-23 20:34:02 +01002139 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002140 with open(os.path.join(FIXTURE_DIR, path)) as f:
2141 tenant_config = yaml.safe_load(f.read())
2142 for tenant in tenant_config:
2143 sources = tenant['tenant']['source']
2144 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002145 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002146 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002147 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002148 self.setupProjectKeys(source, project)
2149
2150 def setupProjectKeys(self, source, project):
2151 # Make sure we set up an RSA key for the project so that we
2152 # don't spend time generating one:
2153
James E. Blair6459db12017-06-29 14:57:20 -07002154 if isinstance(project, dict):
2155 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002156 key_root = os.path.join(self.state_root, 'keys')
2157 if not os.path.isdir(key_root):
2158 os.mkdir(key_root, 0o700)
2159 private_key_file = os.path.join(key_root, source, project + '.pem')
2160 private_key_dir = os.path.dirname(private_key_file)
2161 self.log.debug("Installing test keys for project %s at %s" % (
2162 project, private_key_file))
2163 if not os.path.isdir(private_key_dir):
2164 os.makedirs(private_key_dir)
2165 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2166 with open(private_key_file, 'w') as o:
2167 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002168
James E. Blair498059b2016-12-20 13:50:13 -08002169 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002170 self.zk_chroot_fixture = self.useFixture(
2171 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002172 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002173 self.zk_chroot_fixture.zookeeper_host,
2174 self.zk_chroot_fixture.zookeeper_port,
2175 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002176
James E. Blair96c6bf82016-01-15 16:20:40 -08002177 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002178 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002179
2180 files = {}
2181 for (dirpath, dirnames, filenames) in os.walk(source_path):
2182 for filename in filenames:
2183 test_tree_filepath = os.path.join(dirpath, filename)
2184 common_path = os.path.commonprefix([test_tree_filepath,
2185 source_path])
2186 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2187 with open(test_tree_filepath, 'r') as f:
2188 content = f.read()
2189 files[relative_filepath] = content
2190 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002191 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002192
James E. Blaire18d4602017-01-05 11:17:28 -08002193 def assertNodepoolState(self):
2194 # Make sure that there are no pending requests
2195
2196 requests = self.fake_nodepool.getNodeRequests()
2197 self.assertEqual(len(requests), 0)
2198
2199 nodes = self.fake_nodepool.getNodes()
2200 for node in nodes:
2201 self.assertFalse(node['_lock'], "Node %s is locked" %
2202 (node['_oid'],))
2203
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002204 def assertNoGeneratedKeys(self):
2205 # Make sure that Zuul did not generate any project keys
2206 # (unless it was supposed to).
2207
2208 if self.create_project_keys:
2209 return
2210
2211 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2212 test_key = i.read()
2213
2214 key_root = os.path.join(self.state_root, 'keys')
2215 for root, dirname, files in os.walk(key_root):
2216 for fn in files:
2217 with open(os.path.join(root, fn)) as f:
2218 self.assertEqual(test_key, f.read())
2219
Clark Boylanb640e052014-04-03 16:41:46 -07002220 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002221 self.log.debug("Assert final state")
2222 # Make sure no jobs are running
2223 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002224 # Make sure that git.Repo objects have been garbage collected.
2225 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002226 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002227 gc.collect()
2228 for obj in gc.get_objects():
2229 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002230 self.log.debug("Leaked git repo object: 0x%x %s" %
2231 (id(obj), repr(obj)))
2232 for ref in gc.get_referrers(obj):
2233 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002234 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002235 if repos:
2236 for obj in gc.garbage:
2237 self.log.debug(" Garbage %s" % (repr(obj)))
2238 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002239 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002240 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002241 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002242 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002243 for tenant in self.sched.abide.tenants.values():
2244 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002245 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002246 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002247
2248 def shutdown(self):
2249 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002250 self.executor_server.hold_jobs_in_build = False
2251 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002252 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002253 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002254 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002255 self.sched.stop()
2256 self.sched.join()
2257 self.statsd.stop()
2258 self.statsd.join()
2259 self.webapp.stop()
2260 self.webapp.join()
2261 self.rpc.stop()
2262 self.rpc.join()
2263 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002264 self.fake_nodepool.stop()
2265 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002266 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002267 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002268 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002269 # Further the pydevd threads also need to be whitelisted so debugging
2270 # e.g. in PyCharm is possible without breaking shutdown.
2271 whitelist = ['executor-watchdog',
2272 'pydevd.CommandThread',
2273 'pydevd.Reader',
2274 'pydevd.Writer',
2275 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002276 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002277 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002278 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002279 log_str = ""
2280 for thread_id, stack_frame in sys._current_frames().items():
2281 log_str += "Thread: %s\n" % thread_id
2282 log_str += "".join(traceback.format_stack(stack_frame))
2283 self.log.debug(log_str)
2284 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002285
James E. Blaira002b032017-04-18 10:35:48 -07002286 def assertCleanShutdown(self):
2287 pass
2288
James E. Blairc4ba97a2017-04-19 16:26:24 -07002289 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002290 parts = project.split('/')
2291 path = os.path.join(self.upstream_root, *parts[:-1])
2292 if not os.path.exists(path):
2293 os.makedirs(path)
2294 path = os.path.join(self.upstream_root, project)
2295 repo = git.Repo.init(path)
2296
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002297 with repo.config_writer() as config_writer:
2298 config_writer.set_value('user', 'email', 'user@example.com')
2299 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002300
Clark Boylanb640e052014-04-03 16:41:46 -07002301 repo.index.commit('initial commit')
2302 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002303 if tag:
2304 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002305
James E. Blair97d902e2014-08-21 13:25:56 -07002306 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002307 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002308 repo.git.clean('-x', '-f', '-d')
2309
James E. Blair97d902e2014-08-21 13:25:56 -07002310 def create_branch(self, project, branch):
2311 path = os.path.join(self.upstream_root, project)
2312 repo = git.Repo.init(path)
2313 fn = os.path.join(path, 'README')
2314
2315 branch_head = repo.create_head(branch)
2316 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002317 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002318 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002319 f.close()
2320 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002321 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002322
James E. Blair97d902e2014-08-21 13:25:56 -07002323 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002324 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002325 repo.git.clean('-x', '-f', '-d')
2326
Sachi King9f16d522016-03-16 12:20:45 +11002327 def create_commit(self, project):
2328 path = os.path.join(self.upstream_root, project)
2329 repo = git.Repo(path)
2330 repo.head.reference = repo.heads['master']
2331 file_name = os.path.join(path, 'README')
2332 with open(file_name, 'a') as f:
2333 f.write('creating fake commit\n')
2334 repo.index.add([file_name])
2335 commit = repo.index.commit('Creating a fake commit')
2336 return commit.hexsha
2337
James E. Blairf4a5f022017-04-18 14:01:10 -07002338 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002339 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002340 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002341 while len(self.builds):
2342 self.release(self.builds[0])
2343 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002344 i += 1
2345 if count is not None and i >= count:
2346 break
James E. Blairb8c16472015-05-05 14:55:26 -07002347
James E. Blairdf25ddc2017-07-08 07:57:09 -07002348 def getSortedBuilds(self):
2349 "Return the list of currently running builds sorted by name"
2350
2351 return sorted(self.builds, key=lambda x: x.name)
2352
Clark Boylanb640e052014-04-03 16:41:46 -07002353 def release(self, job):
2354 if isinstance(job, FakeBuild):
2355 job.release()
2356 else:
2357 job.waiting = False
2358 self.log.debug("Queued job %s released" % job.unique)
2359 self.gearman_server.wakeConnections()
2360
2361 def getParameter(self, job, name):
2362 if isinstance(job, FakeBuild):
2363 return job.parameters[name]
2364 else:
2365 parameters = json.loads(job.arguments)
2366 return parameters[name]
2367
Clark Boylanb640e052014-04-03 16:41:46 -07002368 def haveAllBuildsReported(self):
2369 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002370 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002371 return False
2372 # Find out if every build that the worker has completed has been
2373 # reported back to Zuul. If it hasn't then that means a Gearman
2374 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002375 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002376 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002377 if not zbuild:
2378 # It has already been reported
2379 continue
2380 # It hasn't been reported yet.
2381 return False
2382 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002383 worker = self.executor_server.executor_worker
2384 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002385 if connection.state == 'GRAB_WAIT':
2386 return False
2387 return True
2388
2389 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002390 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002391 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002392 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002393 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002394 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002395 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002396 for j in conn.related_jobs.values():
2397 if j.unique == build.uuid:
2398 client_job = j
2399 break
2400 if not client_job:
2401 self.log.debug("%s is not known to the gearman client" %
2402 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002403 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002404 if not client_job.handle:
2405 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002406 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002407 server_job = self.gearman_server.jobs.get(client_job.handle)
2408 if not server_job:
2409 self.log.debug("%s is not known to the gearman server" %
2410 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002411 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002412 if not hasattr(server_job, 'waiting'):
2413 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002414 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002415 if server_job.waiting:
2416 continue
James E. Blair17302972016-08-10 16:11:42 -07002417 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002418 self.log.debug("%s has not reported start" % build)
2419 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002420 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002421 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002422 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002423 if worker_build:
2424 if worker_build.isWaiting():
2425 continue
2426 else:
2427 self.log.debug("%s is running" % worker_build)
2428 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002429 else:
James E. Blair962220f2016-08-03 11:22:38 -07002430 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002431 return False
James E. Blaira002b032017-04-18 10:35:48 -07002432 for (build_uuid, job_worker) in \
2433 self.executor_server.job_workers.items():
2434 if build_uuid not in seen_builds:
2435 self.log.debug("%s is not finalized" % build_uuid)
2436 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002437 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002438
James E. Blairdce6cea2016-12-20 16:45:32 -08002439 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002440 if self.fake_nodepool.paused:
2441 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002442 if self.sched.nodepool.requests:
2443 return False
2444 return True
2445
Jan Hruban6b71aff2015-10-22 16:58:08 +02002446 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002447 for event_queue in self.event_queues:
2448 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002449
2450 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002451 for event_queue in self.event_queues:
2452 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002453
Clark Boylanb640e052014-04-03 16:41:46 -07002454 def waitUntilSettled(self):
2455 self.log.debug("Waiting until settled...")
2456 start = time.time()
2457 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002458 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002459 self.log.error("Timeout waiting for Zuul to settle")
2460 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002461 for event_queue in self.event_queues:
2462 self.log.error(" %s: %s" %
2463 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002464 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002465 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002466 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002467 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002468 self.log.error("All requests completed: %s" %
2469 (self.areAllNodeRequestsComplete(),))
2470 self.log.error("Merge client jobs: %s" %
2471 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002472 raise Exception("Timeout waiting for Zuul to settle")
2473 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002474
Paul Belanger174a8272017-03-14 13:20:10 -04002475 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002476 # have all build states propogated to zuul?
2477 if self.haveAllBuildsReported():
2478 # Join ensures that the queue is empty _and_ events have been
2479 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002480 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002481 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002482 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002483 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002484 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002485 self.areAllNodeRequestsComplete() and
2486 all(self.eventQueuesEmpty())):
2487 # The queue empty check is placed at the end to
2488 # ensure that if a component adds an event between
2489 # when locked the run handler and checked that the
2490 # components were stable, we don't erroneously
2491 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002492 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002493 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002494 self.log.debug("...settled.")
2495 return
2496 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002497 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002498 self.sched.wake_event.wait(0.1)
2499
2500 def countJobResults(self, jobs, result):
2501 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002502 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002503
Monty Taylor0d926122017-05-24 08:07:56 -05002504 def getBuildByName(self, name):
2505 for build in self.builds:
2506 if build.name == name:
2507 return build
2508 raise Exception("Unable to find build %s" % name)
2509
James E. Blair96c6bf82016-01-15 16:20:40 -08002510 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002511 for job in self.history:
2512 if (job.name == name and
2513 (project is None or
2514 job.parameters['ZUUL_PROJECT'] == project)):
2515 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002516 raise Exception("Unable to find job %s in history" % name)
2517
2518 def assertEmptyQueues(self):
2519 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002520 for tenant in self.sched.abide.tenants.values():
2521 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002522 for pipeline_queue in pipeline.queues:
2523 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002524 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002525 pipeline.name, pipeline_queue.name,
2526 pipeline_queue.queue))
2527 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002528 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002529
2530 def assertReportedStat(self, key, value=None, kind=None):
2531 start = time.time()
2532 while time.time() < (start + 5):
2533 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002534 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002535 if key == k:
2536 if value is None and kind is None:
2537 return
2538 elif value:
2539 if value == v:
2540 return
2541 elif kind:
2542 if v.endswith('|' + kind):
2543 return
2544 time.sleep(0.1)
2545
Clark Boylanb640e052014-04-03 16:41:46 -07002546 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002547
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002548 def assertBuilds(self, builds):
2549 """Assert that the running builds are as described.
2550
2551 The list of running builds is examined and must match exactly
2552 the list of builds described by the input.
2553
2554 :arg list builds: A list of dictionaries. Each item in the
2555 list must match the corresponding build in the build
2556 history, and each element of the dictionary must match the
2557 corresponding attribute of the build.
2558
2559 """
James E. Blair3158e282016-08-19 09:34:11 -07002560 try:
2561 self.assertEqual(len(self.builds), len(builds))
2562 for i, d in enumerate(builds):
2563 for k, v in d.items():
2564 self.assertEqual(
2565 getattr(self.builds[i], k), v,
2566 "Element %i in builds does not match" % (i,))
2567 except Exception:
2568 for build in self.builds:
2569 self.log.error("Running build: %s" % build)
2570 else:
2571 self.log.error("No running builds")
2572 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002573
James E. Blairb536ecc2016-08-31 10:11:42 -07002574 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002575 """Assert that the completed builds are as described.
2576
2577 The list of completed builds is examined and must match
2578 exactly the list of builds described by the input.
2579
2580 :arg list history: A list of dictionaries. Each item in the
2581 list must match the corresponding build in the build
2582 history, and each element of the dictionary must match the
2583 corresponding attribute of the build.
2584
James E. Blairb536ecc2016-08-31 10:11:42 -07002585 :arg bool ordered: If true, the history must match the order
2586 supplied, if false, the builds are permitted to have
2587 arrived in any order.
2588
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002589 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002590 def matches(history_item, item):
2591 for k, v in item.items():
2592 if getattr(history_item, k) != v:
2593 return False
2594 return True
James E. Blair3158e282016-08-19 09:34:11 -07002595 try:
2596 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002597 if ordered:
2598 for i, d in enumerate(history):
2599 if not matches(self.history[i], d):
2600 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002601 "Element %i in history does not match %s" %
2602 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002603 else:
2604 unseen = self.history[:]
2605 for i, d in enumerate(history):
2606 found = False
2607 for unseen_item in unseen:
2608 if matches(unseen_item, d):
2609 found = True
2610 unseen.remove(unseen_item)
2611 break
2612 if not found:
2613 raise Exception("No match found for element %i "
2614 "in history" % (i,))
2615 if unseen:
2616 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002617 except Exception:
2618 for build in self.history:
2619 self.log.error("Completed build: %s" % build)
2620 else:
2621 self.log.error("No completed builds")
2622 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002623
James E. Blair6ac368c2016-12-22 18:07:20 -08002624 def printHistory(self):
2625 """Log the build history.
2626
2627 This can be useful during tests to summarize what jobs have
2628 completed.
2629
2630 """
2631 self.log.debug("Build history:")
2632 for build in self.history:
2633 self.log.debug(build)
2634
James E. Blair59fdbac2015-12-07 17:08:06 -08002635 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002636 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2637
James E. Blair9ea70072017-04-19 16:05:30 -07002638 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002639 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002640 if not os.path.exists(root):
2641 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002642 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2643 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002644- tenant:
2645 name: openstack
2646 source:
2647 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002648 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002649 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002650 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002651 - org/project
2652 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002653 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002654 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002655 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002656 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002657 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002658
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002659 def addCommitToRepo(self, project, message, files,
2660 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002661 path = os.path.join(self.upstream_root, project)
2662 repo = git.Repo(path)
2663 repo.head.reference = branch
2664 zuul.merger.merger.reset_repo_to_head(repo)
2665 for fn, content in files.items():
2666 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002667 try:
2668 os.makedirs(os.path.dirname(fn))
2669 except OSError:
2670 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002671 with open(fn, 'w') as f:
2672 f.write(content)
2673 repo.index.add([fn])
2674 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002675 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002676 repo.heads[branch].commit = commit
2677 repo.head.reference = branch
2678 repo.git.clean('-x', '-f', '-d')
2679 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002680 if tag:
2681 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002682 return before
2683
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002684 def commitConfigUpdate(self, project_name, source_name):
2685 """Commit an update to zuul.yaml
2686
2687 This overwrites the zuul.yaml in the specificed project with
2688 the contents specified.
2689
2690 :arg str project_name: The name of the project containing
2691 zuul.yaml (e.g., common-config)
2692
2693 :arg str source_name: The path to the file (underneath the
2694 test fixture directory) whose contents should be used to
2695 replace zuul.yaml.
2696 """
2697
2698 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002699 files = {}
2700 with open(source_path, 'r') as f:
2701 data = f.read()
2702 layout = yaml.safe_load(data)
2703 files['zuul.yaml'] = data
2704 for item in layout:
2705 if 'job' in item:
2706 jobname = item['job']['name']
2707 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002708 before = self.addCommitToRepo(
2709 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002710 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002711 return before
2712
James E. Blair7fc8daa2016-08-08 15:37:15 -07002713 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002714
James E. Blair7fc8daa2016-08-08 15:37:15 -07002715 """Inject a Fake (Gerrit) event.
2716
2717 This method accepts a JSON-encoded event and simulates Zuul
2718 having received it from Gerrit. It could (and should)
2719 eventually apply to any connection type, but is currently only
2720 used with Gerrit connections. The name of the connection is
2721 used to look up the corresponding server, and the event is
2722 simulated as having been received by all Zuul connections
2723 attached to that server. So if two Gerrit connections in Zuul
2724 are connected to the same Gerrit server, and you invoke this
2725 method specifying the name of one of them, the event will be
2726 received by both.
2727
2728 .. note::
2729
2730 "self.fake_gerrit.addEvent" calls should be migrated to
2731 this method.
2732
2733 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002734 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002735 :arg str event: The JSON-encoded event.
2736
2737 """
2738 specified_conn = self.connections.connections[connection]
2739 for conn in self.connections.connections.values():
2740 if (isinstance(conn, specified_conn.__class__) and
2741 specified_conn.server == conn.server):
2742 conn.addEvent(event)
2743
James E. Blaird8af5422017-05-24 13:59:40 -07002744 def getUpstreamRepos(self, projects):
2745 """Return upstream git repo objects for the listed projects
2746
2747 :arg list projects: A list of strings, each the canonical name
2748 of a project.
2749
2750 :returns: A dictionary of {name: repo} for every listed
2751 project.
2752 :rtype: dict
2753
2754 """
2755
2756 repos = {}
2757 for project in projects:
2758 # FIXME(jeblair): the upstream root does not yet have a
2759 # hostname component; that needs to be added, and this
2760 # line removed:
2761 tmp_project_name = '/'.join(project.split('/')[1:])
2762 path = os.path.join(self.upstream_root, tmp_project_name)
2763 repo = git.Repo(path)
2764 repos[project] = repo
2765 return repos
2766
James E. Blair3f876d52016-07-22 13:07:14 -07002767
2768class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002769 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002770 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002771
Joshua Heskethd78b4482015-09-14 16:56:34 -06002772
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002773class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002774 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002775 use_ssl = True
2776
2777
Joshua Heskethd78b4482015-09-14 16:56:34 -06002778class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002779 def setup_config(self):
2780 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002781 for section_name in self.config.sections():
2782 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2783 section_name, re.I)
2784 if not con_match:
2785 continue
2786
2787 if self.config.get(section_name, 'driver') == 'sql':
2788 f = MySQLSchemaFixture()
2789 self.useFixture(f)
2790 if (self.config.get(section_name, 'dburi') ==
2791 '$MYSQL_FIXTURE_DBURI$'):
2792 self.config.set(section_name, 'dburi', f.dburi)