blob: fb946385fed938ff51a2909a6070805255a8b0b4 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050022import importlib
23from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import statsd
51import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080052import testtools.content
53import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080054from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000055import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070056
James E. Blaire511d2f2016-12-08 15:22:26 -080057import zuul.driver.gerrit.gerritsource as gerritsource
58import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070059import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070060import zuul.scheduler
61import zuul.webapp
62import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020069import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080071import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010072from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700136 categories = {'approved': ('Approved', -1, 1),
137 'code-review': ('Code-Review', -2, 2),
138 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700139
140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700141 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700176 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700183 ref = GerritChangeReference.create(
184 repo, '1/%s/%s' % (self.number, self.latest_patchset),
185 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700187 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.git.clean('-x', '-f', '-d')
189
190 path = os.path.join(self.upstream_root, self.project)
191 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for fn, content in files.items():
193 fn = os.path.join(path, fn)
194 with open(fn, 'w') as f:
195 f.write(content)
196 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700197 else:
198 for fni in range(100):
199 fn = os.path.join(path, str(fni))
200 f = open(fn, 'w')
201 for ci in range(4096):
202 f.write(random.choice(string.printable))
203 f.close()
204 repo.index.add([fn])
205
206 r = repo.index.commit(msg)
207 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700208 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700209 repo.git.clean('-x', '-f', '-d')
210 repo.heads['master'].checkout()
211 return r
212
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700214 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700216 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 data = ("test %s %s %s\n" %
218 (self.branch, self.number, self.latest_patchset))
219 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700220 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 ps_files = [{'file': '/COMMIT_MSG',
223 'type': 'ADDED'},
224 {'file': 'README',
225 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700227 ps_files.append({'file': f, 'type': 'ADDED'})
228 d = {'approvals': [],
229 'createdOn': time.time(),
230 'files': ps_files,
231 'number': str(self.latest_patchset),
232 'ref': 'refs/changes/1/%s/%s' % (self.number,
233 self.latest_patchset),
234 'revision': c.hexsha,
235 'uploader': {'email': 'user@example.com',
236 'name': 'User name',
237 'username': 'user'}}
238 self.data['currentPatchSet'] = d
239 self.patchsets.append(d)
240 self.data['submitRecords'] = self.getSubmitRecords()
241
242 def getPatchsetCreatedEvent(self, patchset):
243 event = {"type": "patchset-created",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "patchSet": self.patchsets[patchset - 1],
252 "uploader": {"name": "User Name"}}
253 return event
254
255 def getChangeRestoredEvent(self):
256 event = {"type": "change-restored",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100265 "patchSet": self.patchsets[-1],
266 "reason": ""}
267 return event
268
269 def getChangeAbandonedEvent(self):
270 event = {"type": "change-abandoned",
271 "change": {"project": self.project,
272 "branch": self.branch,
273 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
274 "number": str(self.number),
275 "subject": self.subject,
276 "owner": {"name": "User Name"},
277 "url": "https://hostname/3"},
278 "abandoner": {"name": "User Name"},
279 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700280 "reason": ""}
281 return event
282
283 def getChangeCommentEvent(self, patchset):
284 event = {"type": "comment-added",
285 "change": {"project": self.project,
286 "branch": self.branch,
287 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
288 "number": str(self.number),
289 "subject": self.subject,
290 "owner": {"name": "User Name"},
291 "url": "https://hostname/3"},
292 "patchSet": self.patchsets[patchset - 1],
293 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700294 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700295 "description": "Code-Review",
296 "value": "0"}],
297 "comment": "This is a comment"}
298 return event
299
James E. Blairc2a5ed72017-02-20 14:12:01 -0500300 def getChangeMergedEvent(self):
301 event = {"submitter": {"name": "Jenkins",
302 "username": "jenkins"},
303 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
304 "patchSet": self.patchsets[-1],
305 "change": self.data,
306 "type": "change-merged",
307 "eventCreatedOn": 1487613810}
308 return event
309
James E. Blair8cce42e2016-10-18 08:18:36 -0700310 def getRefUpdatedEvent(self):
311 path = os.path.join(self.upstream_root, self.project)
312 repo = git.Repo(path)
313 oldrev = repo.heads[self.branch].commit.hexsha
314
315 event = {
316 "type": "ref-updated",
317 "submitter": {
318 "name": "User Name",
319 },
320 "refUpdate": {
321 "oldRev": oldrev,
322 "newRev": self.patchsets[-1]['revision'],
323 "refName": self.branch,
324 "project": self.project,
325 }
326 }
327 return event
328
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 def addApproval(self, category, value, username='reviewer_john',
330 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700331 if not granted_on:
332 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000333 approval = {
334 'description': self.categories[category][0],
335 'type': category,
336 'value': str(value),
337 'by': {
338 'username': username,
339 'email': username + '@example.com',
340 },
341 'grantedOn': int(granted_on)
342 }
Clark Boylanb640e052014-04-03 16:41:46 -0700343 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
344 if x['by']['username'] == username and x['type'] == category:
345 del self.patchsets[-1]['approvals'][i]
346 self.patchsets[-1]['approvals'].append(approval)
347 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'author': {'email': 'author@example.com',
349 'name': 'Patchset Author',
350 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'change': {'branch': self.branch,
352 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
353 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'owner': {'email': 'owner@example.com',
355 'name': 'Change Owner',
356 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700357 'project': self.project,
358 'subject': self.subject,
359 'topic': 'master',
360 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'patchSet': self.patchsets[-1],
363 'type': 'comment-added'}
364 self.data['submitRecords'] = self.getSubmitRecords()
365 return json.loads(json.dumps(event))
366
367 def getSubmitRecords(self):
368 status = {}
369 for cat in self.categories.keys():
370 status[cat] = 0
371
372 for a in self.patchsets[-1]['approvals']:
373 cur = status[a['type']]
374 cat_min, cat_max = self.categories[a['type']][1:]
375 new = int(a['value'])
376 if new == cat_min:
377 cur = new
378 elif abs(new) > abs(cur):
379 cur = new
380 status[a['type']] = cur
381
382 labels = []
383 ok = True
384 for typ, cat in self.categories.items():
385 cur = status[typ]
386 cat_min, cat_max = cat[1:]
387 if cur == cat_min:
388 value = 'REJECT'
389 ok = False
390 elif cur == cat_max:
391 value = 'OK'
392 else:
393 value = 'NEED'
394 ok = False
395 labels.append({'label': cat[0], 'status': value})
396 if ok:
397 return [{'status': 'OK'}]
398 return [{'status': 'NOT_READY',
399 'labels': labels}]
400
401 def setDependsOn(self, other, patchset):
402 self.depends_on_change = other
403 d = {'id': other.data['id'],
404 'number': other.data['number'],
405 'ref': other.patchsets[patchset - 1]['ref']
406 }
407 self.data['dependsOn'] = [d]
408
409 other.needed_by_changes.append(self)
410 needed = other.data.get('neededBy', [])
411 d = {'id': self.data['id'],
412 'number': self.data['number'],
413 'ref': self.patchsets[patchset - 1]['ref'],
414 'revision': self.patchsets[patchset - 1]['revision']
415 }
416 needed.append(d)
417 other.data['neededBy'] = needed
418
419 def query(self):
420 self.queried += 1
421 d = self.data.get('dependsOn')
422 if d:
423 d = d[0]
424 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
425 d['isCurrentPatchSet'] = True
426 else:
427 d['isCurrentPatchSet'] = False
428 return json.loads(json.dumps(self.data))
429
430 def setMerged(self):
431 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000432 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700433 return
434 if self.fail_merge:
435 return
436 self.data['status'] = 'MERGED'
437 self.open = False
438
439 path = os.path.join(self.upstream_root, self.project)
440 repo = git.Repo(path)
441 repo.heads[self.branch].commit = \
442 repo.commit(self.patchsets[-1]['revision'])
443
444 def setReported(self):
445 self.reported += 1
446
447
James E. Blaire511d2f2016-12-08 15:22:26 -0800448class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700449 """A Fake Gerrit connection for use in tests.
450
451 This subclasses
452 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
453 ability for tests to add changes to the fake Gerrit it represents.
454 """
455
Joshua Hesketh352264b2015-08-11 23:42:08 +1000456 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700459 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000461 connection_config)
462
Monty Taylorb934c1a2017-06-16 19:31:47 -0500463 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700464 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
465 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200468 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700469
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def addFakeChange(self, project, branch, subject, status='NEW',
471 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700472 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700474 c = FakeGerritChange(self, self.change_number, project, branch,
475 subject, upstream_root=self.upstream_root,
476 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.changes[self.change_number] = c
478 return c
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480 def review(self, project, changeid, message, action):
481 number, ps = changeid.split(',')
482 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000483
484 # Add the approval back onto the change (ie simulate what gerrit would
485 # do).
486 # Usually when zuul leaves a review it'll create a feedback loop where
487 # zuul's review enters another gerrit event (which is then picked up by
488 # zuul). However, we can't mimic this behaviour (by adding this
489 # approval event into the queue) as it stops jobs from checking what
490 # happens before this event is triggered. If a job needs to see what
491 # happens they can add their own verified event into the queue.
492 # Nevertheless, we can update change with the new review in gerrit.
493
James E. Blair8b5408c2016-08-08 15:37:46 -0700494 for cat in action.keys():
495 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000496 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
Clark Boylanb640e052014-04-03 16:41:46 -0700498 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 if 'submit' in action:
501 change.setMerged()
502 if message:
503 change.setReported()
504
505 def query(self, number):
506 change = self.changes.get(int(number))
507 if change:
508 return change.query()
509 return {}
510
James E. Blairc494d542014-08-06 09:23:52 -0700511 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700512 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700513 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800514 if query.startswith('change:'):
515 # Query a specific changeid
516 changeid = query[len('change:'):]
517 l = [change.query() for change in self.changes.values()
518 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700519 elif query.startswith('message:'):
520 # Query the content of a commit message
521 msg = query[len('message:'):].strip()
522 l = [change.query() for change in self.changes.values()
523 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800524 else:
525 # Query all open changes
526 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700527 return l
James E. Blairc494d542014-08-06 09:23:52 -0700528
Joshua Hesketh352264b2015-08-11 23:42:08 +1000529 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700530 pass
531
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200532 def _uploadPack(self, project):
533 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
534 'multi_ack thin-pack side-band side-band-64k ofs-delta '
535 'shallow no-progress include-tag multi_ack_detailed no-done\n')
536 path = os.path.join(self.upstream_root, project.name)
537 repo = git.Repo(path)
538 for ref in repo.refs:
539 r = ref.object.hexsha + ' ' + ref.path + '\n'
540 ret += '%04x%s' % (len(r) + 4, r)
541 ret += '0000'
542 return ret
543
Joshua Hesketh352264b2015-08-11 23:42:08 +1000544 def getGitUrl(self, project):
545 return os.path.join(self.upstream_root, project.name)
546
Clark Boylanb640e052014-04-03 16:41:46 -0700547
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548class GithubChangeReference(git.Reference):
549 _common_path_default = "refs/pull"
550 _points_to_commits_only = True
551
552
553class FakeGithubPullRequest(object):
554
555 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800556 subject, upstream_root, files=[], number_of_commits=1,
Jesse 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. Blair17302972016-08-10 16:11:42 -07001071 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1072 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001073
1074
Clark Boylanb640e052014-04-03 16:41:46 -07001075class FakeStatsd(threading.Thread):
1076 def __init__(self):
1077 threading.Thread.__init__(self)
1078 self.daemon = True
1079 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1080 self.sock.bind(('', 0))
1081 self.port = self.sock.getsockname()[1]
1082 self.wake_read, self.wake_write = os.pipe()
1083 self.stats = []
1084
1085 def run(self):
1086 while True:
1087 poll = select.poll()
1088 poll.register(self.sock, select.POLLIN)
1089 poll.register(self.wake_read, select.POLLIN)
1090 ret = poll.poll()
1091 for (fd, event) in ret:
1092 if fd == self.sock.fileno():
1093 data = self.sock.recvfrom(1024)
1094 if not data:
1095 return
1096 self.stats.append(data[0])
1097 if fd == self.wake_read:
1098 return
1099
1100 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001101 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001102
1103
James E. Blaire1767bc2016-08-02 10:00:27 -07001104class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001105 log = logging.getLogger("zuul.test")
1106
Paul Belanger174a8272017-03-14 13:20:10 -04001107 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001108 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001109 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001110 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001111 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001112 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001113 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001114 # TODOv3(jeblair): self.node is really "the label of the node
1115 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001116 # keep using it like this, or we may end up exposing more of
1117 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001118 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001119 self.node = None
1120 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001121 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001123 self.pipeline = self.parameters['ZUUL_PIPELINE']
1124 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001125 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001126 self.wait_condition = threading.Condition()
1127 self.waiting = False
1128 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001129 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001130 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001131 self.changes = None
1132 if 'ZUUL_CHANGE_IDS' in self.parameters:
1133 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001134
James E. Blair3158e282016-08-19 09:34:11 -07001135 def __repr__(self):
1136 waiting = ''
1137 if self.waiting:
1138 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001139 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1140 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001141
Clark Boylanb640e052014-04-03 16:41:46 -07001142 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001143 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001144 self.wait_condition.acquire()
1145 self.wait_condition.notify()
1146 self.waiting = False
1147 self.log.debug("Build %s released" % self.unique)
1148 self.wait_condition.release()
1149
1150 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001151 """Return whether this build is being held.
1152
1153 :returns: Whether the build is being held.
1154 :rtype: bool
1155 """
1156
Clark Boylanb640e052014-04-03 16:41:46 -07001157 self.wait_condition.acquire()
1158 if self.waiting:
1159 ret = True
1160 else:
1161 ret = False
1162 self.wait_condition.release()
1163 return ret
1164
1165 def _wait(self):
1166 self.wait_condition.acquire()
1167 self.waiting = True
1168 self.log.debug("Build %s waiting" % self.unique)
1169 self.wait_condition.wait()
1170 self.wait_condition.release()
1171
1172 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001173 self.log.debug('Running build %s' % self.unique)
1174
Paul Belanger174a8272017-03-14 13:20:10 -04001175 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001176 self.log.debug('Holding build %s' % self.unique)
1177 self._wait()
1178 self.log.debug("Build %s continuing" % self.unique)
1179
James E. Blair412fba82017-01-26 15:00:50 -08001180 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001181 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001182 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001183 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001184 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001185 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001186 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001187
James E. Blaire1767bc2016-08-02 10:00:27 -07001188 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001189
James E. Blaira5dba232016-08-08 15:53:24 -07001190 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001191 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001192 for change in changes:
1193 if self.hasChanges(change):
1194 return True
1195 return False
1196
James E. Blaire7b99a02016-08-05 14:27:34 -07001197 def hasChanges(self, *changes):
1198 """Return whether this build has certain changes in its git repos.
1199
1200 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001201 are expected to be present (in order) in the git repository of
1202 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001203
1204 :returns: Whether the build has the indicated changes.
1205 :rtype: bool
1206
1207 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001208 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001209 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001210 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001211 try:
1212 repo = git.Repo(path)
1213 except NoSuchPathError as e:
1214 self.log.debug('%s' % e)
1215 return False
1216 ref = self.parameters['ZUUL_REF']
1217 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1218 commit_message = '%s-1' % change.subject
1219 self.log.debug("Checking if build %s has changes; commit_message "
1220 "%s; repo_messages %s" % (self, commit_message,
1221 repo_messages))
1222 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001223 self.log.debug(" messages do not match")
1224 return False
1225 self.log.debug(" OK")
1226 return True
1227
James E. Blaird8af5422017-05-24 13:59:40 -07001228 def getWorkspaceRepos(self, projects):
1229 """Return workspace git repo objects for the listed projects
1230
1231 :arg list projects: A list of strings, each the canonical name
1232 of a project.
1233
1234 :returns: A dictionary of {name: repo} for every listed
1235 project.
1236 :rtype: dict
1237
1238 """
1239
1240 repos = {}
1241 for project in projects:
1242 path = os.path.join(self.jobdir.src_root, project)
1243 repo = git.Repo(path)
1244 repos[project] = repo
1245 return repos
1246
Clark Boylanb640e052014-04-03 16:41:46 -07001247
Paul Belanger174a8272017-03-14 13:20:10 -04001248class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1249 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001250
Paul Belanger174a8272017-03-14 13:20:10 -04001251 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001252 they will report that they have started but then pause until
1253 released before reporting completion. This attribute may be
1254 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001255 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001256 be explicitly released.
1257
1258 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001259 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001260 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001261 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001262 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001263 self.hold_jobs_in_build = False
1264 self.lock = threading.Lock()
1265 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001266 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001267 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001268 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001269
James E. Blaira5dba232016-08-08 15:53:24 -07001270 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001271 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001272
1273 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001274 :arg Change change: The :py:class:`~tests.base.FakeChange`
1275 instance which should cause the job to fail. This job
1276 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001277
1278 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001279 l = self.fail_tests.get(name, [])
1280 l.append(change)
1281 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001282
James E. Blair962220f2016-08-03 11:22:38 -07001283 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001284 """Release a held build.
1285
1286 :arg str regex: A regular expression which, if supplied, will
1287 cause only builds with matching names to be released. If
1288 not supplied, all builds will be released.
1289
1290 """
James E. Blair962220f2016-08-03 11:22:38 -07001291 builds = self.running_builds[:]
1292 self.log.debug("Releasing build %s (%s)" % (regex,
1293 len(self.running_builds)))
1294 for build in builds:
1295 if not regex or re.match(regex, build.name):
1296 self.log.debug("Releasing build %s" %
1297 (build.parameters['ZUUL_UUID']))
1298 build.release()
1299 else:
1300 self.log.debug("Not releasing build %s" %
1301 (build.parameters['ZUUL_UUID']))
1302 self.log.debug("Done releasing builds %s (%s)" %
1303 (regex, len(self.running_builds)))
1304
Paul Belanger174a8272017-03-14 13:20:10 -04001305 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001306 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001307 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001308 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001309 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001310 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001311 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001312 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001313 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1314 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001315
1316 def stopJob(self, job):
1317 self.log.debug("handle stop")
1318 parameters = json.loads(job.arguments)
1319 uuid = parameters['uuid']
1320 for build in self.running_builds:
1321 if build.unique == uuid:
1322 build.aborted = True
1323 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001324 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001325
James E. Blaira002b032017-04-18 10:35:48 -07001326 def stop(self):
1327 for build in self.running_builds:
1328 build.release()
1329 super(RecordingExecutorServer, self).stop()
1330
Joshua Hesketh50c21782016-10-13 21:34:14 +11001331
Paul Belanger174a8272017-03-14 13:20:10 -04001332class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001333 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001334 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001335 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001336 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001337 if not commit: # merge conflict
1338 self.recordResult('MERGER_FAILURE')
1339 return commit
1340
1341 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001342 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001343 self.executor_server.lock.acquire()
1344 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001345 BuildHistory(name=build.name, result=result, changes=build.changes,
1346 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001347 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001348 pipeline=build.parameters['ZUUL_PIPELINE'])
1349 )
Paul Belanger174a8272017-03-14 13:20:10 -04001350 self.executor_server.running_builds.remove(build)
1351 del self.executor_server.job_builds[self.job.unique]
1352 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001353
1354 def runPlaybooks(self, args):
1355 build = self.executor_server.job_builds[self.job.unique]
1356 build.jobdir = self.jobdir
1357
1358 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1359 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001360 return result
1361
James E. Blair74a82cf2017-07-12 17:23:08 -07001362 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001363 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001364
Paul Belanger174a8272017-03-14 13:20:10 -04001365 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001366 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001367 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001368 else:
1369 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001370 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001371
James E. Blairad8dca02017-02-21 11:48:32 -05001372 def getHostList(self, args):
1373 self.log.debug("hostlist")
1374 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001375 for host in hosts:
1376 host['host_vars']['ansible_connection'] = 'local'
1377
1378 hosts.append(dict(
1379 name='localhost',
1380 host_vars=dict(ansible_connection='local'),
1381 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001382 return hosts
1383
James E. Blairf5dbd002015-12-23 15:26:17 -08001384
Clark Boylanb640e052014-04-03 16:41:46 -07001385class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001386 """A Gearman server for use in tests.
1387
1388 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1389 added to the queue but will not be distributed to workers
1390 until released. This attribute may be changed at any time and
1391 will take effect for subsequently enqueued jobs, but
1392 previously held jobs will still need to be explicitly
1393 released.
1394
1395 """
1396
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001397 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001398 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001399 if use_ssl:
1400 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1401 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1402 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1403 else:
1404 ssl_ca = None
1405 ssl_cert = None
1406 ssl_key = None
1407
1408 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1409 ssl_cert=ssl_cert,
1410 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001411
1412 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001413 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1414 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001415 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001416 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001417 job.waiting = self.hold_jobs_in_queue
1418 else:
1419 job.waiting = False
1420 if job.waiting:
1421 continue
1422 if job.name in connection.functions:
1423 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001424 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001425 connection.related_jobs[job.handle] = job
1426 job.worker_connection = connection
1427 job.running = True
1428 return job
1429 return None
1430
1431 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001432 """Release a held job.
1433
1434 :arg str regex: A regular expression which, if supplied, will
1435 cause only jobs with matching names to be released. If
1436 not supplied, all jobs will be released.
1437 """
Clark Boylanb640e052014-04-03 16:41:46 -07001438 released = False
1439 qlen = (len(self.high_queue) + len(self.normal_queue) +
1440 len(self.low_queue))
1441 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1442 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001443 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001444 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001445 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001446 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001447 self.log.debug("releasing queued job %s" %
1448 job.unique)
1449 job.waiting = False
1450 released = True
1451 else:
1452 self.log.debug("not releasing queued job %s" %
1453 job.unique)
1454 if released:
1455 self.wakeConnections()
1456 qlen = (len(self.high_queue) + len(self.normal_queue) +
1457 len(self.low_queue))
1458 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1459
1460
1461class FakeSMTP(object):
1462 log = logging.getLogger('zuul.FakeSMTP')
1463
1464 def __init__(self, messages, server, port):
1465 self.server = server
1466 self.port = port
1467 self.messages = messages
1468
1469 def sendmail(self, from_email, to_email, msg):
1470 self.log.info("Sending email from %s, to %s, with msg %s" % (
1471 from_email, to_email, msg))
1472
1473 headers = msg.split('\n\n', 1)[0]
1474 body = msg.split('\n\n', 1)[1]
1475
1476 self.messages.append(dict(
1477 from_email=from_email,
1478 to_email=to_email,
1479 msg=msg,
1480 headers=headers,
1481 body=body,
1482 ))
1483
1484 return True
1485
1486 def quit(self):
1487 return True
1488
1489
James E. Blairdce6cea2016-12-20 16:45:32 -08001490class FakeNodepool(object):
1491 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001492 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001493
1494 log = logging.getLogger("zuul.test.FakeNodepool")
1495
1496 def __init__(self, host, port, chroot):
1497 self.client = kazoo.client.KazooClient(
1498 hosts='%s:%s%s' % (host, port, chroot))
1499 self.client.start()
1500 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001501 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001502 self.thread = threading.Thread(target=self.run)
1503 self.thread.daemon = True
1504 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001505 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001506
1507 def stop(self):
1508 self._running = False
1509 self.thread.join()
1510 self.client.stop()
1511 self.client.close()
1512
1513 def run(self):
1514 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001515 try:
1516 self._run()
1517 except Exception:
1518 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001519 time.sleep(0.1)
1520
1521 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001522 if self.paused:
1523 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001524 for req in self.getNodeRequests():
1525 self.fulfillRequest(req)
1526
1527 def getNodeRequests(self):
1528 try:
1529 reqids = self.client.get_children(self.REQUEST_ROOT)
1530 except kazoo.exceptions.NoNodeError:
1531 return []
1532 reqs = []
1533 for oid in sorted(reqids):
1534 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001535 try:
1536 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001537 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001538 data['_oid'] = oid
1539 reqs.append(data)
1540 except kazoo.exceptions.NoNodeError:
1541 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001542 return reqs
1543
James E. Blaire18d4602017-01-05 11:17:28 -08001544 def getNodes(self):
1545 try:
1546 nodeids = self.client.get_children(self.NODE_ROOT)
1547 except kazoo.exceptions.NoNodeError:
1548 return []
1549 nodes = []
1550 for oid in sorted(nodeids):
1551 path = self.NODE_ROOT + '/' + oid
1552 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001553 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001554 data['_oid'] = oid
1555 try:
1556 lockfiles = self.client.get_children(path + '/lock')
1557 except kazoo.exceptions.NoNodeError:
1558 lockfiles = []
1559 if lockfiles:
1560 data['_lock'] = True
1561 else:
1562 data['_lock'] = False
1563 nodes.append(data)
1564 return nodes
1565
James E. Blaira38c28e2017-01-04 10:33:20 -08001566 def makeNode(self, request_id, node_type):
1567 now = time.time()
1568 path = '/nodepool/nodes/'
1569 data = dict(type=node_type,
1570 provider='test-provider',
1571 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001572 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001573 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001574 public_ipv4='127.0.0.1',
1575 private_ipv4=None,
1576 public_ipv6=None,
1577 allocated_to=request_id,
1578 state='ready',
1579 state_time=now,
1580 created_time=now,
1581 updated_time=now,
1582 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001583 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001584 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001585 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001586 path = self.client.create(path, data,
1587 makepath=True,
1588 sequence=True)
1589 nodeid = path.split("/")[-1]
1590 return nodeid
1591
James E. Blair6ab79e02017-01-06 10:10:17 -08001592 def addFailRequest(self, request):
1593 self.fail_requests.add(request['_oid'])
1594
James E. Blairdce6cea2016-12-20 16:45:32 -08001595 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001596 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001597 return
1598 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001599 oid = request['_oid']
1600 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001601
James E. Blair6ab79e02017-01-06 10:10:17 -08001602 if oid in self.fail_requests:
1603 request['state'] = 'failed'
1604 else:
1605 request['state'] = 'fulfilled'
1606 nodes = []
1607 for node in request['node_types']:
1608 nodeid = self.makeNode(oid, node)
1609 nodes.append(nodeid)
1610 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001611
James E. Blaira38c28e2017-01-04 10:33:20 -08001612 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001613 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001614 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001615 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001616 try:
1617 self.client.set(path, data)
1618 except kazoo.exceptions.NoNodeError:
1619 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001620
1621
James E. Blair498059b2016-12-20 13:50:13 -08001622class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001623 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001624 super(ChrootedKazooFixture, self).__init__()
1625
1626 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1627 if ':' in zk_host:
1628 host, port = zk_host.split(':')
1629 else:
1630 host = zk_host
1631 port = None
1632
1633 self.zookeeper_host = host
1634
1635 if not port:
1636 self.zookeeper_port = 2181
1637 else:
1638 self.zookeeper_port = int(port)
1639
Clark Boylan621ec9a2017-04-07 17:41:33 -07001640 self.test_id = test_id
1641
James E. Blair498059b2016-12-20 13:50:13 -08001642 def _setUp(self):
1643 # Make sure the test chroot paths do not conflict
1644 random_bits = ''.join(random.choice(string.ascii_lowercase +
1645 string.ascii_uppercase)
1646 for x in range(8))
1647
Clark Boylan621ec9a2017-04-07 17:41:33 -07001648 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001649 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1650
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001651 self.addCleanup(self._cleanup)
1652
James E. Blair498059b2016-12-20 13:50:13 -08001653 # Ensure the chroot path exists and clean up any pre-existing znodes.
1654 _tmp_client = kazoo.client.KazooClient(
1655 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1656 _tmp_client.start()
1657
1658 if _tmp_client.exists(self.zookeeper_chroot):
1659 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1660
1661 _tmp_client.ensure_path(self.zookeeper_chroot)
1662 _tmp_client.stop()
1663 _tmp_client.close()
1664
James E. Blair498059b2016-12-20 13:50:13 -08001665 def _cleanup(self):
1666 '''Remove the chroot path.'''
1667 # Need a non-chroot'ed client to remove the chroot path
1668 _tmp_client = kazoo.client.KazooClient(
1669 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1670 _tmp_client.start()
1671 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1672 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001673 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001674
1675
Joshua Heskethd78b4482015-09-14 16:56:34 -06001676class MySQLSchemaFixture(fixtures.Fixture):
1677 def setUp(self):
1678 super(MySQLSchemaFixture, self).setUp()
1679
1680 random_bits = ''.join(random.choice(string.ascii_lowercase +
1681 string.ascii_uppercase)
1682 for x in range(8))
1683 self.name = '%s_%s' % (random_bits, os.getpid())
1684 self.passwd = uuid.uuid4().hex
1685 db = pymysql.connect(host="localhost",
1686 user="openstack_citest",
1687 passwd="openstack_citest",
1688 db="openstack_citest")
1689 cur = db.cursor()
1690 cur.execute("create database %s" % self.name)
1691 cur.execute(
1692 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1693 (self.name, self.name, self.passwd))
1694 cur.execute("flush privileges")
1695
1696 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1697 self.passwd,
1698 self.name)
1699 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1700 self.addCleanup(self.cleanup)
1701
1702 def cleanup(self):
1703 db = pymysql.connect(host="localhost",
1704 user="openstack_citest",
1705 passwd="openstack_citest",
1706 db="openstack_citest")
1707 cur = db.cursor()
1708 cur.execute("drop database %s" % self.name)
1709 cur.execute("drop user '%s'@'localhost'" % self.name)
1710 cur.execute("flush privileges")
1711
1712
Maru Newby3fe5f852015-01-13 04:22:14 +00001713class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001714 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001715 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001716
James E. Blair1c236df2017-02-01 14:07:24 -08001717 def attachLogs(self, *args):
1718 def reader():
1719 self._log_stream.seek(0)
1720 while True:
1721 x = self._log_stream.read(4096)
1722 if not x:
1723 break
1724 yield x.encode('utf8')
1725 content = testtools.content.content_from_reader(
1726 reader,
1727 testtools.content_type.UTF8_TEXT,
1728 False)
1729 self.addDetail('logging', content)
1730
Clark Boylanb640e052014-04-03 16:41:46 -07001731 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001732 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001733 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1734 try:
1735 test_timeout = int(test_timeout)
1736 except ValueError:
1737 # If timeout value is invalid do not set a timeout.
1738 test_timeout = 0
1739 if test_timeout > 0:
1740 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1741
1742 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1743 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1744 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1745 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1746 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1747 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1748 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1749 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1750 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1751 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001752 self._log_stream = StringIO()
1753 self.addOnException(self.attachLogs)
1754 else:
1755 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001756
James E. Blair73b41772017-05-22 13:22:55 -07001757 # NOTE(jeblair): this is temporary extra debugging to try to
1758 # track down a possible leak.
1759 orig_git_repo_init = git.Repo.__init__
1760
1761 def git_repo_init(myself, *args, **kw):
1762 orig_git_repo_init(myself, *args, **kw)
1763 self.log.debug("Created git repo 0x%x %s" %
1764 (id(myself), repr(myself)))
1765
1766 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1767 git_repo_init))
1768
James E. Blair1c236df2017-02-01 14:07:24 -08001769 handler = logging.StreamHandler(self._log_stream)
1770 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1771 '%(levelname)-8s %(message)s')
1772 handler.setFormatter(formatter)
1773
1774 logger = logging.getLogger()
1775 logger.setLevel(logging.DEBUG)
1776 logger.addHandler(handler)
1777
Clark Boylan3410d532017-04-25 12:35:29 -07001778 # Make sure we don't carry old handlers around in process state
1779 # which slows down test runs
1780 self.addCleanup(logger.removeHandler, handler)
1781 self.addCleanup(handler.close)
1782 self.addCleanup(handler.flush)
1783
James E. Blair1c236df2017-02-01 14:07:24 -08001784 # NOTE(notmorgan): Extract logging overrides for specific
1785 # libraries from the OS_LOG_DEFAULTS env and create loggers
1786 # for each. This is used to limit the output during test runs
1787 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001788 log_defaults_from_env = os.environ.get(
1789 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001790 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001791
James E. Blairdce6cea2016-12-20 16:45:32 -08001792 if log_defaults_from_env:
1793 for default in log_defaults_from_env.split(','):
1794 try:
1795 name, level_str = default.split('=', 1)
1796 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001797 logger = logging.getLogger(name)
1798 logger.setLevel(level)
1799 logger.addHandler(handler)
1800 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001801 except ValueError:
1802 # NOTE(notmorgan): Invalid format of the log default,
1803 # skip and don't try and apply a logger for the
1804 # specified module
1805 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001806
Maru Newby3fe5f852015-01-13 04:22:14 +00001807
1808class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001809 """A test case with a functioning Zuul.
1810
1811 The following class variables are used during test setup and can
1812 be overidden by subclasses but are effectively read-only once a
1813 test method starts running:
1814
1815 :cvar str config_file: This points to the main zuul config file
1816 within the fixtures directory. Subclasses may override this
1817 to obtain a different behavior.
1818
1819 :cvar str tenant_config_file: This is the tenant config file
1820 (which specifies from what git repos the configuration should
1821 be loaded). It defaults to the value specified in
1822 `config_file` but can be overidden by subclasses to obtain a
1823 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001824 configuration. See also the :py:func:`simple_layout`
1825 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001826
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001827 :cvar bool create_project_keys: Indicates whether Zuul should
1828 auto-generate keys for each project, or whether the test
1829 infrastructure should insert dummy keys to save time during
1830 startup. Defaults to False.
1831
James E. Blaire7b99a02016-08-05 14:27:34 -07001832 The following are instance variables that are useful within test
1833 methods:
1834
1835 :ivar FakeGerritConnection fake_<connection>:
1836 A :py:class:`~tests.base.FakeGerritConnection` will be
1837 instantiated for each connection present in the config file
1838 and stored here. For instance, `fake_gerrit` will hold the
1839 FakeGerritConnection object for a connection named `gerrit`.
1840
1841 :ivar FakeGearmanServer gearman_server: An instance of
1842 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1843 server that all of the Zuul components in this test use to
1844 communicate with each other.
1845
Paul Belanger174a8272017-03-14 13:20:10 -04001846 :ivar RecordingExecutorServer executor_server: An instance of
1847 :py:class:`~tests.base.RecordingExecutorServer` which is the
1848 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001849
1850 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1851 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001852 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001853 list upon completion.
1854
1855 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1856 objects representing completed builds. They are appended to
1857 the list in the order they complete.
1858
1859 """
1860
James E. Blair83005782015-12-11 14:46:03 -08001861 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001862 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001863 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001864 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001865
1866 def _startMerger(self):
1867 self.merge_server = zuul.merger.server.MergeServer(self.config,
1868 self.connections)
1869 self.merge_server.start()
1870
Maru Newby3fe5f852015-01-13 04:22:14 +00001871 def setUp(self):
1872 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001873
1874 self.setupZK()
1875
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001876 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001877 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001878 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1879 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001880 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001881 tmp_root = tempfile.mkdtemp(
1882 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001883 self.test_root = os.path.join(tmp_root, "zuul-test")
1884 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001885 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001886 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001887 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001888 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1889 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001890
1891 if os.path.exists(self.test_root):
1892 shutil.rmtree(self.test_root)
1893 os.makedirs(self.test_root)
1894 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001895 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001896 os.makedirs(self.merger_state_root)
1897 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001898
1899 # Make per test copy of Configuration.
1900 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001901 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1902 if not os.path.exists(self.private_key_file):
1903 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1904 shutil.copy(src_private_key_file, self.private_key_file)
1905 shutil.copy('{}.pub'.format(src_private_key_file),
1906 '{}.pub'.format(self.private_key_file))
1907 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001908 self.config.set('scheduler', 'tenant_config',
1909 os.path.join(
1910 FIXTURE_DIR,
1911 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001912 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001913 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001914 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001915 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001916 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001917 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001918
Clark Boylanb640e052014-04-03 16:41:46 -07001919 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001920 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1921 # see: https://github.com/jsocol/pystatsd/issues/61
1922 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001923 os.environ['STATSD_PORT'] = str(self.statsd.port)
1924 self.statsd.start()
1925 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001926 importlib.reload(statsd)
1927 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001928
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001929 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001930
1931 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001932 self.log.info("Gearman server on port %s" %
1933 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001934 if self.use_ssl:
1935 self.log.info('SSL enabled for gearman')
1936 self.config.set(
1937 'gearman', 'ssl_ca',
1938 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1939 self.config.set(
1940 'gearman', 'ssl_cert',
1941 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1942 self.config.set(
1943 'gearman', 'ssl_key',
1944 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001945
James E. Blaire511d2f2016-12-08 15:22:26 -08001946 gerritsource.GerritSource.replication_timeout = 1.5
1947 gerritsource.GerritSource.replication_retry_interval = 0.5
1948 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001949
Joshua Hesketh352264b2015-08-11 23:42:08 +10001950 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001951
Jan Hruban7083edd2015-08-21 14:00:54 +02001952 self.webapp = zuul.webapp.WebApp(
1953 self.sched, port=0, listen_address='127.0.0.1')
1954
Jan Hruban6b71aff2015-10-22 16:58:08 +02001955 self.event_queues = [
1956 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001957 self.sched.trigger_event_queue,
1958 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001959 ]
1960
James E. Blairfef78942016-03-11 16:28:56 -08001961 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001962 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001963
Paul Belanger174a8272017-03-14 13:20:10 -04001964 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001965 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001966 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001967 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001968 _test_root=self.test_root,
1969 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001970 self.executor_server.start()
1971 self.history = self.executor_server.build_history
1972 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001973
Paul Belanger174a8272017-03-14 13:20:10 -04001974 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001975 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001976 self.merge_client = zuul.merger.client.MergeClient(
1977 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001978 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001979 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001980 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001981
James E. Blair0d5a36e2017-02-21 10:53:44 -05001982 self.fake_nodepool = FakeNodepool(
1983 self.zk_chroot_fixture.zookeeper_host,
1984 self.zk_chroot_fixture.zookeeper_port,
1985 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001986
Paul Belanger174a8272017-03-14 13:20:10 -04001987 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001988 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001989 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001990 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001991
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001992 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001993
1994 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001995 self.webapp.start()
1996 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001997 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001998 # Cleanups are run in reverse order
1999 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002000 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002001 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002002
James E. Blairb9c0d772017-03-03 14:34:49 -08002003 self.sched.reconfigure(self.config)
2004 self.sched.resume()
2005
Tobias Henkel7df274b2017-05-26 17:41:11 +02002006 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002007 # Set up gerrit related fakes
2008 # Set a changes database so multiple FakeGerrit's can report back to
2009 # a virtual canonical database given by the configured hostname
2010 self.gerrit_changes_dbs = {}
2011
2012 def getGerritConnection(driver, name, config):
2013 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2014 con = FakeGerritConnection(driver, name, config,
2015 changes_db=db,
2016 upstream_root=self.upstream_root)
2017 self.event_queues.append(con.event_queue)
2018 setattr(self, 'fake_' + name, con)
2019 return con
2020
2021 self.useFixture(fixtures.MonkeyPatch(
2022 'zuul.driver.gerrit.GerritDriver.getConnection',
2023 getGerritConnection))
2024
Gregory Haynes4fc12542015-04-22 20:38:06 -07002025 def getGithubConnection(driver, name, config):
2026 con = FakeGithubConnection(driver, name, config,
2027 upstream_root=self.upstream_root)
2028 setattr(self, 'fake_' + name, con)
2029 return con
2030
2031 self.useFixture(fixtures.MonkeyPatch(
2032 'zuul.driver.github.GithubDriver.getConnection',
2033 getGithubConnection))
2034
James E. Blaire511d2f2016-12-08 15:22:26 -08002035 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002036 # TODO(jhesketh): This should come from lib.connections for better
2037 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002038 # Register connections from the config
2039 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002040
Joshua Hesketh352264b2015-08-11 23:42:08 +10002041 def FakeSMTPFactory(*args, **kw):
2042 args = [self.smtp_messages] + list(args)
2043 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002044
Joshua Hesketh352264b2015-08-11 23:42:08 +10002045 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002046
James E. Blaire511d2f2016-12-08 15:22:26 -08002047 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002048 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002049 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002050
James E. Blair83005782015-12-11 14:46:03 -08002051 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002052 # This creates the per-test configuration object. It can be
2053 # overriden by subclasses, but should not need to be since it
2054 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002055 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002056 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002057
James E. Blair39840362017-06-23 20:34:02 +01002058 sections = ['zuul', 'scheduler', 'executor', 'merger']
2059 for section in sections:
2060 if not self.config.has_section(section):
2061 self.config.add_section(section)
2062
James E. Blair06cc3922017-04-19 10:08:10 -07002063 if not self.setupSimpleLayout():
2064 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002065 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002066 self.tenant_config_file)
2067 git_path = os.path.join(
2068 os.path.dirname(
2069 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2070 'git')
2071 if os.path.exists(git_path):
2072 for reponame in os.listdir(git_path):
2073 project = reponame.replace('_', '/')
2074 self.copyDirToRepo(project,
2075 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002076 # Make test_root persist after ansible run for .flag test
2077 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002078 self.setupAllProjectKeys()
2079
James E. Blair06cc3922017-04-19 10:08:10 -07002080 def setupSimpleLayout(self):
2081 # If the test method has been decorated with a simple_layout,
2082 # use that instead of the class tenant_config_file. Set up a
2083 # single config-project with the specified layout, and
2084 # initialize repos for all of the 'project' entries which
2085 # appear in the layout.
2086 test_name = self.id().split('.')[-1]
2087 test = getattr(self, test_name)
2088 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002089 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002090 else:
2091 return False
2092
James E. Blairb70e55a2017-04-19 12:57:02 -07002093 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002094 path = os.path.join(FIXTURE_DIR, path)
2095 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002096 data = f.read()
2097 layout = yaml.safe_load(data)
2098 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002099 untrusted_projects = []
2100 for item in layout:
2101 if 'project' in item:
2102 name = item['project']['name']
2103 untrusted_projects.append(name)
2104 self.init_repo(name)
2105 self.addCommitToRepo(name, 'initial commit',
2106 files={'README': ''},
2107 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002108 if 'job' in item:
2109 jobname = item['job']['name']
2110 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002111
2112 root = os.path.join(self.test_root, "config")
2113 if not os.path.exists(root):
2114 os.makedirs(root)
2115 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2116 config = [{'tenant':
2117 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002118 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002119 {'config-projects': ['common-config'],
2120 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002121 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002122 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002123 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002124 os.path.join(FIXTURE_DIR, f.name))
2125
2126 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002127 self.addCommitToRepo('common-config', 'add content from fixture',
2128 files, branch='master', tag='init')
2129
2130 return True
2131
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002132 def setupAllProjectKeys(self):
2133 if self.create_project_keys:
2134 return
2135
James E. Blair39840362017-06-23 20:34:02 +01002136 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002137 with open(os.path.join(FIXTURE_DIR, path)) as f:
2138 tenant_config = yaml.safe_load(f.read())
2139 for tenant in tenant_config:
2140 sources = tenant['tenant']['source']
2141 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002142 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002143 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002144 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002145 self.setupProjectKeys(source, project)
2146
2147 def setupProjectKeys(self, source, project):
2148 # Make sure we set up an RSA key for the project so that we
2149 # don't spend time generating one:
2150
James E. Blair6459db12017-06-29 14:57:20 -07002151 if isinstance(project, dict):
2152 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002153 key_root = os.path.join(self.state_root, 'keys')
2154 if not os.path.isdir(key_root):
2155 os.mkdir(key_root, 0o700)
2156 private_key_file = os.path.join(key_root, source, project + '.pem')
2157 private_key_dir = os.path.dirname(private_key_file)
2158 self.log.debug("Installing test keys for project %s at %s" % (
2159 project, private_key_file))
2160 if not os.path.isdir(private_key_dir):
2161 os.makedirs(private_key_dir)
2162 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2163 with open(private_key_file, 'w') as o:
2164 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002165
James E. Blair498059b2016-12-20 13:50:13 -08002166 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002167 self.zk_chroot_fixture = self.useFixture(
2168 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002169 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002170 self.zk_chroot_fixture.zookeeper_host,
2171 self.zk_chroot_fixture.zookeeper_port,
2172 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002173
James E. Blair96c6bf82016-01-15 16:20:40 -08002174 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002175 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002176
2177 files = {}
2178 for (dirpath, dirnames, filenames) in os.walk(source_path):
2179 for filename in filenames:
2180 test_tree_filepath = os.path.join(dirpath, filename)
2181 common_path = os.path.commonprefix([test_tree_filepath,
2182 source_path])
2183 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2184 with open(test_tree_filepath, 'r') as f:
2185 content = f.read()
2186 files[relative_filepath] = content
2187 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002188 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002189
James E. Blaire18d4602017-01-05 11:17:28 -08002190 def assertNodepoolState(self):
2191 # Make sure that there are no pending requests
2192
2193 requests = self.fake_nodepool.getNodeRequests()
2194 self.assertEqual(len(requests), 0)
2195
2196 nodes = self.fake_nodepool.getNodes()
2197 for node in nodes:
2198 self.assertFalse(node['_lock'], "Node %s is locked" %
2199 (node['_oid'],))
2200
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002201 def assertNoGeneratedKeys(self):
2202 # Make sure that Zuul did not generate any project keys
2203 # (unless it was supposed to).
2204
2205 if self.create_project_keys:
2206 return
2207
2208 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2209 test_key = i.read()
2210
2211 key_root = os.path.join(self.state_root, 'keys')
2212 for root, dirname, files in os.walk(key_root):
2213 for fn in files:
2214 with open(os.path.join(root, fn)) as f:
2215 self.assertEqual(test_key, f.read())
2216
Clark Boylanb640e052014-04-03 16:41:46 -07002217 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002218 self.log.debug("Assert final state")
2219 # Make sure no jobs are running
2220 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002221 # Make sure that git.Repo objects have been garbage collected.
2222 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002223 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002224 gc.collect()
2225 for obj in gc.get_objects():
2226 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002227 self.log.debug("Leaked git repo object: 0x%x %s" %
2228 (id(obj), repr(obj)))
2229 for ref in gc.get_referrers(obj):
2230 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002231 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002232 if repos:
2233 for obj in gc.garbage:
2234 self.log.debug(" Garbage %s" % (repr(obj)))
2235 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002236 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002237 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002238 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002239 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002240 for tenant in self.sched.abide.tenants.values():
2241 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002242 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002243 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002244
2245 def shutdown(self):
2246 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002247 self.executor_server.hold_jobs_in_build = False
2248 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002249 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002250 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002251 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002252 self.sched.stop()
2253 self.sched.join()
2254 self.statsd.stop()
2255 self.statsd.join()
2256 self.webapp.stop()
2257 self.webapp.join()
2258 self.rpc.stop()
2259 self.rpc.join()
2260 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002261 self.fake_nodepool.stop()
2262 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002263 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002264 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002265 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002266 # Further the pydevd threads also need to be whitelisted so debugging
2267 # e.g. in PyCharm is possible without breaking shutdown.
2268 whitelist = ['executor-watchdog',
2269 'pydevd.CommandThread',
2270 'pydevd.Reader',
2271 'pydevd.Writer',
2272 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002273 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002274 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002275 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002276 log_str = ""
2277 for thread_id, stack_frame in sys._current_frames().items():
2278 log_str += "Thread: %s\n" % thread_id
2279 log_str += "".join(traceback.format_stack(stack_frame))
2280 self.log.debug(log_str)
2281 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002282
James E. Blaira002b032017-04-18 10:35:48 -07002283 def assertCleanShutdown(self):
2284 pass
2285
James E. Blairc4ba97a2017-04-19 16:26:24 -07002286 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002287 parts = project.split('/')
2288 path = os.path.join(self.upstream_root, *parts[:-1])
2289 if not os.path.exists(path):
2290 os.makedirs(path)
2291 path = os.path.join(self.upstream_root, project)
2292 repo = git.Repo.init(path)
2293
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002294 with repo.config_writer() as config_writer:
2295 config_writer.set_value('user', 'email', 'user@example.com')
2296 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002297
Clark Boylanb640e052014-04-03 16:41:46 -07002298 repo.index.commit('initial commit')
2299 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002300 if tag:
2301 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002302
James E. Blair97d902e2014-08-21 13:25:56 -07002303 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002304 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002305 repo.git.clean('-x', '-f', '-d')
2306
James E. Blair97d902e2014-08-21 13:25:56 -07002307 def create_branch(self, project, branch):
2308 path = os.path.join(self.upstream_root, project)
2309 repo = git.Repo.init(path)
2310 fn = os.path.join(path, 'README')
2311
2312 branch_head = repo.create_head(branch)
2313 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002314 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002315 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002316 f.close()
2317 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002318 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002319
James E. Blair97d902e2014-08-21 13:25:56 -07002320 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002321 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002322 repo.git.clean('-x', '-f', '-d')
2323
Sachi King9f16d522016-03-16 12:20:45 +11002324 def create_commit(self, project):
2325 path = os.path.join(self.upstream_root, project)
2326 repo = git.Repo(path)
2327 repo.head.reference = repo.heads['master']
2328 file_name = os.path.join(path, 'README')
2329 with open(file_name, 'a') as f:
2330 f.write('creating fake commit\n')
2331 repo.index.add([file_name])
2332 commit = repo.index.commit('Creating a fake commit')
2333 return commit.hexsha
2334
James E. Blairf4a5f022017-04-18 14:01:10 -07002335 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002336 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002337 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002338 while len(self.builds):
2339 self.release(self.builds[0])
2340 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002341 i += 1
2342 if count is not None and i >= count:
2343 break
James E. Blairb8c16472015-05-05 14:55:26 -07002344
James E. Blairdf25ddc2017-07-08 07:57:09 -07002345 def getSortedBuilds(self):
2346 "Return the list of currently running builds sorted by name"
2347
2348 return sorted(self.builds, key=lambda x: x.name)
2349
Clark Boylanb640e052014-04-03 16:41:46 -07002350 def release(self, job):
2351 if isinstance(job, FakeBuild):
2352 job.release()
2353 else:
2354 job.waiting = False
2355 self.log.debug("Queued job %s released" % job.unique)
2356 self.gearman_server.wakeConnections()
2357
2358 def getParameter(self, job, name):
2359 if isinstance(job, FakeBuild):
2360 return job.parameters[name]
2361 else:
2362 parameters = json.loads(job.arguments)
2363 return parameters[name]
2364
Clark Boylanb640e052014-04-03 16:41:46 -07002365 def haveAllBuildsReported(self):
2366 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002367 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002368 return False
2369 # Find out if every build that the worker has completed has been
2370 # reported back to Zuul. If it hasn't then that means a Gearman
2371 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002372 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002373 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002374 if not zbuild:
2375 # It has already been reported
2376 continue
2377 # It hasn't been reported yet.
2378 return False
2379 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002380 worker = self.executor_server.executor_worker
2381 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002382 if connection.state == 'GRAB_WAIT':
2383 return False
2384 return True
2385
2386 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002387 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002388 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002389 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002390 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002391 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002392 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002393 for j in conn.related_jobs.values():
2394 if j.unique == build.uuid:
2395 client_job = j
2396 break
2397 if not client_job:
2398 self.log.debug("%s is not known to the gearman client" %
2399 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002400 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002401 if not client_job.handle:
2402 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002403 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002404 server_job = self.gearman_server.jobs.get(client_job.handle)
2405 if not server_job:
2406 self.log.debug("%s is not known to the gearman server" %
2407 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002408 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002409 if not hasattr(server_job, 'waiting'):
2410 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002411 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002412 if server_job.waiting:
2413 continue
James E. Blair17302972016-08-10 16:11:42 -07002414 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002415 self.log.debug("%s has not reported start" % build)
2416 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002417 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002418 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002419 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002420 if worker_build:
2421 if worker_build.isWaiting():
2422 continue
2423 else:
2424 self.log.debug("%s is running" % worker_build)
2425 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002426 else:
James E. Blair962220f2016-08-03 11:22:38 -07002427 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002428 return False
James E. Blaira002b032017-04-18 10:35:48 -07002429 for (build_uuid, job_worker) in \
2430 self.executor_server.job_workers.items():
2431 if build_uuid not in seen_builds:
2432 self.log.debug("%s is not finalized" % build_uuid)
2433 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002434 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002435
James E. Blairdce6cea2016-12-20 16:45:32 -08002436 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002437 if self.fake_nodepool.paused:
2438 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002439 if self.sched.nodepool.requests:
2440 return False
2441 return True
2442
Jan Hruban6b71aff2015-10-22 16:58:08 +02002443 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002444 for event_queue in self.event_queues:
2445 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002446
2447 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002448 for event_queue in self.event_queues:
2449 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002450
Clark Boylanb640e052014-04-03 16:41:46 -07002451 def waitUntilSettled(self):
2452 self.log.debug("Waiting until settled...")
2453 start = time.time()
2454 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002455 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002456 self.log.error("Timeout waiting for Zuul to settle")
2457 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002458 for event_queue in self.event_queues:
2459 self.log.error(" %s: %s" %
2460 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002461 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002462 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002463 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002464 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002465 self.log.error("All requests completed: %s" %
2466 (self.areAllNodeRequestsComplete(),))
2467 self.log.error("Merge client jobs: %s" %
2468 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002469 raise Exception("Timeout waiting for Zuul to settle")
2470 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002471
Paul Belanger174a8272017-03-14 13:20:10 -04002472 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002473 # have all build states propogated to zuul?
2474 if self.haveAllBuildsReported():
2475 # Join ensures that the queue is empty _and_ events have been
2476 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002477 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002478 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002479 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002480 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002481 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002482 self.areAllNodeRequestsComplete() and
2483 all(self.eventQueuesEmpty())):
2484 # The queue empty check is placed at the end to
2485 # ensure that if a component adds an event between
2486 # when locked the run handler and checked that the
2487 # components were stable, we don't erroneously
2488 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002489 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002490 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002491 self.log.debug("...settled.")
2492 return
2493 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002494 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002495 self.sched.wake_event.wait(0.1)
2496
2497 def countJobResults(self, jobs, result):
2498 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002499 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002500
Monty Taylor0d926122017-05-24 08:07:56 -05002501 def getBuildByName(self, name):
2502 for build in self.builds:
2503 if build.name == name:
2504 return build
2505 raise Exception("Unable to find build %s" % name)
2506
James E. Blair96c6bf82016-01-15 16:20:40 -08002507 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002508 for job in self.history:
2509 if (job.name == name and
2510 (project is None or
2511 job.parameters['ZUUL_PROJECT'] == project)):
2512 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002513 raise Exception("Unable to find job %s in history" % name)
2514
2515 def assertEmptyQueues(self):
2516 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002517 for tenant in self.sched.abide.tenants.values():
2518 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002519 for pipeline_queue in pipeline.queues:
2520 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002521 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002522 pipeline.name, pipeline_queue.name,
2523 pipeline_queue.queue))
2524 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002525 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002526
2527 def assertReportedStat(self, key, value=None, kind=None):
2528 start = time.time()
2529 while time.time() < (start + 5):
2530 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002531 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002532 if key == k:
2533 if value is None and kind is None:
2534 return
2535 elif value:
2536 if value == v:
2537 return
2538 elif kind:
2539 if v.endswith('|' + kind):
2540 return
2541 time.sleep(0.1)
2542
Clark Boylanb640e052014-04-03 16:41:46 -07002543 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002544
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002545 def assertBuilds(self, builds):
2546 """Assert that the running builds are as described.
2547
2548 The list of running builds is examined and must match exactly
2549 the list of builds described by the input.
2550
2551 :arg list builds: A list of dictionaries. Each item in the
2552 list must match the corresponding build in the build
2553 history, and each element of the dictionary must match the
2554 corresponding attribute of the build.
2555
2556 """
James E. Blair3158e282016-08-19 09:34:11 -07002557 try:
2558 self.assertEqual(len(self.builds), len(builds))
2559 for i, d in enumerate(builds):
2560 for k, v in d.items():
2561 self.assertEqual(
2562 getattr(self.builds[i], k), v,
2563 "Element %i in builds does not match" % (i,))
2564 except Exception:
2565 for build in self.builds:
2566 self.log.error("Running build: %s" % build)
2567 else:
2568 self.log.error("No running builds")
2569 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002570
James E. Blairb536ecc2016-08-31 10:11:42 -07002571 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002572 """Assert that the completed builds are as described.
2573
2574 The list of completed builds is examined and must match
2575 exactly the list of builds described by the input.
2576
2577 :arg list history: A list of dictionaries. Each item in the
2578 list must match the corresponding build in the build
2579 history, and each element of the dictionary must match the
2580 corresponding attribute of the build.
2581
James E. Blairb536ecc2016-08-31 10:11:42 -07002582 :arg bool ordered: If true, the history must match the order
2583 supplied, if false, the builds are permitted to have
2584 arrived in any order.
2585
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002586 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002587 def matches(history_item, item):
2588 for k, v in item.items():
2589 if getattr(history_item, k) != v:
2590 return False
2591 return True
James E. Blair3158e282016-08-19 09:34:11 -07002592 try:
2593 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002594 if ordered:
2595 for i, d in enumerate(history):
2596 if not matches(self.history[i], d):
2597 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002598 "Element %i in history does not match %s" %
2599 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002600 else:
2601 unseen = self.history[:]
2602 for i, d in enumerate(history):
2603 found = False
2604 for unseen_item in unseen:
2605 if matches(unseen_item, d):
2606 found = True
2607 unseen.remove(unseen_item)
2608 break
2609 if not found:
2610 raise Exception("No match found for element %i "
2611 "in history" % (i,))
2612 if unseen:
2613 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002614 except Exception:
2615 for build in self.history:
2616 self.log.error("Completed build: %s" % build)
2617 else:
2618 self.log.error("No completed builds")
2619 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002620
James E. Blair6ac368c2016-12-22 18:07:20 -08002621 def printHistory(self):
2622 """Log the build history.
2623
2624 This can be useful during tests to summarize what jobs have
2625 completed.
2626
2627 """
2628 self.log.debug("Build history:")
2629 for build in self.history:
2630 self.log.debug(build)
2631
James E. Blair59fdbac2015-12-07 17:08:06 -08002632 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002633 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2634
James E. Blair9ea70072017-04-19 16:05:30 -07002635 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002636 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002637 if not os.path.exists(root):
2638 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002639 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2640 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002641- tenant:
2642 name: openstack
2643 source:
2644 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002645 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002646 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002647 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002648 - org/project
2649 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002650 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002651 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002652 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002653 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002654 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002655
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002656 def addCommitToRepo(self, project, message, files,
2657 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002658 path = os.path.join(self.upstream_root, project)
2659 repo = git.Repo(path)
2660 repo.head.reference = branch
2661 zuul.merger.merger.reset_repo_to_head(repo)
2662 for fn, content in files.items():
2663 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002664 try:
2665 os.makedirs(os.path.dirname(fn))
2666 except OSError:
2667 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002668 with open(fn, 'w') as f:
2669 f.write(content)
2670 repo.index.add([fn])
2671 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002672 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002673 repo.heads[branch].commit = commit
2674 repo.head.reference = branch
2675 repo.git.clean('-x', '-f', '-d')
2676 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002677 if tag:
2678 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002679 return before
2680
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002681 def commitConfigUpdate(self, project_name, source_name):
2682 """Commit an update to zuul.yaml
2683
2684 This overwrites the zuul.yaml in the specificed project with
2685 the contents specified.
2686
2687 :arg str project_name: The name of the project containing
2688 zuul.yaml (e.g., common-config)
2689
2690 :arg str source_name: The path to the file (underneath the
2691 test fixture directory) whose contents should be used to
2692 replace zuul.yaml.
2693 """
2694
2695 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002696 files = {}
2697 with open(source_path, 'r') as f:
2698 data = f.read()
2699 layout = yaml.safe_load(data)
2700 files['zuul.yaml'] = data
2701 for item in layout:
2702 if 'job' in item:
2703 jobname = item['job']['name']
2704 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002705 before = self.addCommitToRepo(
2706 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002707 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002708 return before
2709
James E. Blair7fc8daa2016-08-08 15:37:15 -07002710 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002711
James E. Blair7fc8daa2016-08-08 15:37:15 -07002712 """Inject a Fake (Gerrit) event.
2713
2714 This method accepts a JSON-encoded event and simulates Zuul
2715 having received it from Gerrit. It could (and should)
2716 eventually apply to any connection type, but is currently only
2717 used with Gerrit connections. The name of the connection is
2718 used to look up the corresponding server, and the event is
2719 simulated as having been received by all Zuul connections
2720 attached to that server. So if two Gerrit connections in Zuul
2721 are connected to the same Gerrit server, and you invoke this
2722 method specifying the name of one of them, the event will be
2723 received by both.
2724
2725 .. note::
2726
2727 "self.fake_gerrit.addEvent" calls should be migrated to
2728 this method.
2729
2730 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002731 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002732 :arg str event: The JSON-encoded event.
2733
2734 """
2735 specified_conn = self.connections.connections[connection]
2736 for conn in self.connections.connections.values():
2737 if (isinstance(conn, specified_conn.__class__) and
2738 specified_conn.server == conn.server):
2739 conn.addEvent(event)
2740
James E. Blaird8af5422017-05-24 13:59:40 -07002741 def getUpstreamRepos(self, projects):
2742 """Return upstream git repo objects for the listed projects
2743
2744 :arg list projects: A list of strings, each the canonical name
2745 of a project.
2746
2747 :returns: A dictionary of {name: repo} for every listed
2748 project.
2749 :rtype: dict
2750
2751 """
2752
2753 repos = {}
2754 for project in projects:
2755 # FIXME(jeblair): the upstream root does not yet have a
2756 # hostname component; that needs to be added, and this
2757 # line removed:
2758 tmp_project_name = '/'.join(project.split('/')[1:])
2759 path = os.path.join(self.upstream_root, tmp_project_name)
2760 repo = git.Repo(path)
2761 repos[project] = repo
2762 return repos
2763
James E. Blair3f876d52016-07-22 13:07:14 -07002764
2765class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002766 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002767 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002768
Joshua Heskethd78b4482015-09-14 16:56:34 -06002769
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002770class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002771 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002772 use_ssl = True
2773
2774
Joshua Heskethd78b4482015-09-14 16:56:34 -06002775class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002776 def setup_config(self):
2777 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002778 for section_name in self.config.sections():
2779 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2780 section_name, re.I)
2781 if not con_match:
2782 continue
2783
2784 if self.config.get(section_name, 'driver') == 'sql':
2785 f = MySQLSchemaFixture()
2786 self.useFixture(f)
2787 if (self.config.get(section_name, 'dburi') ==
2788 '$MYSQL_FIXTURE_DBURI$'):
2789 self.config.set(section_name, 'dburi', f.dburi)