blob: 944af8092fec7bf3679c0130ff9255bf1f1428aa [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
Jamie Lennox7655b552017-03-17 12:33:38 +110019from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080020import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070021import gc
22import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050023import importlib
24from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070025import json
26import logging
27import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050028import queue
Clark Boylanb640e052014-04-03 16:41:46 -070029import random
30import re
31import select
32import shutil
33import socket
34import string
35import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
Clark Boylan8208c192017-04-24 18:08:08 -070039import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070040import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060041import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050042import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060043
Clark Boylanb640e052014-04-03 16:41:46 -070044
45import git
46import gear
47import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080048import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080049import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060050import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070051import statsd
52import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080053import testtools.content
54import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080055from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000056import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070057
James E. Blaire511d2f2016-12-08 15:22:26 -080058import zuul.driver.gerrit.gerritsource as gerritsource
59import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070060import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.scheduler
62import zuul.webapp
63import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040064import zuul.executor.server
65import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080066import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070067import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070068import zuul.merger.merger
69import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020070import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200137 categories = {'Approved': ('Approved', -1, 1),
138 'Code-Review': ('Code-Review', -2, 2),
139 'Verified': ('Verified', -2, 2)}
140
Clark Boylanb640e052014-04-03 16:41:46 -0700141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700142 status='NEW', upstream_root=None, files={},
143 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700145 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.reported = 0
147 self.queried = 0
148 self.patchsets = []
149 self.number = number
150 self.project = project
151 self.branch = branch
152 self.subject = subject
153 self.latest_patchset = 0
154 self.depends_on_change = None
155 self.needed_by_changes = []
156 self.fail_merge = False
157 self.messages = []
158 self.data = {
159 'branch': branch,
160 'comments': [],
161 'commitMessage': subject,
162 'createdOn': time.time(),
163 'id': 'I' + random_sha1(),
164 'lastUpdated': time.time(),
165 'number': str(number),
166 'open': status == 'NEW',
167 'owner': {'email': 'user@example.com',
168 'name': 'User Name',
169 'username': 'username'},
170 'patchSets': self.patchsets,
171 'project': project,
172 'status': status,
173 'subject': subject,
174 'submitRecords': [],
175 'url': 'https://hostname/%s' % number}
176
177 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700178 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 self.data['submitRecords'] = self.getSubmitRecords()
180 self.open = status == 'NEW'
181
James E. Blair289f5932017-07-27 15:02:29 -0700182 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 path = os.path.join(self.upstream_root, self.project)
184 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700185 if parent is None:
186 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700187 ref = GerritChangeReference.create(
188 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700189 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700191 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700192 repo.git.clean('-x', '-f', '-d')
193
194 path = os.path.join(self.upstream_root, self.project)
195 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700196 for fn, content in files.items():
197 fn = os.path.join(path, fn)
198 with open(fn, 'w') as f:
199 f.write(content)
200 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700201 else:
202 for fni in range(100):
203 fn = os.path.join(path, str(fni))
204 f = open(fn, 'w')
205 for ci in range(4096):
206 f.write(random.choice(string.printable))
207 f.close()
208 repo.index.add([fn])
209
210 r = repo.index.commit(msg)
211 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700212 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700213 repo.git.clean('-x', '-f', '-d')
214 repo.heads['master'].checkout()
215 return r
216
James E. Blair289f5932017-07-27 15:02:29 -0700217 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700218 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700219 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700220 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 data = ("test %s %s %s\n" %
222 (self.branch, self.number, self.latest_patchset))
223 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700224 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700225 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700226 ps_files = [{'file': '/COMMIT_MSG',
227 'type': 'ADDED'},
228 {'file': 'README',
229 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700230 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700231 ps_files.append({'file': f, 'type': 'ADDED'})
232 d = {'approvals': [],
233 'createdOn': time.time(),
234 'files': ps_files,
235 'number': str(self.latest_patchset),
236 'ref': 'refs/changes/1/%s/%s' % (self.number,
237 self.latest_patchset),
238 'revision': c.hexsha,
239 'uploader': {'email': 'user@example.com',
240 'name': 'User name',
241 'username': 'user'}}
242 self.data['currentPatchSet'] = d
243 self.patchsets.append(d)
244 self.data['submitRecords'] = self.getSubmitRecords()
245
246 def getPatchsetCreatedEvent(self, patchset):
247 event = {"type": "patchset-created",
248 "change": {"project": self.project,
249 "branch": self.branch,
250 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
251 "number": str(self.number),
252 "subject": self.subject,
253 "owner": {"name": "User Name"},
254 "url": "https://hostname/3"},
255 "patchSet": self.patchsets[patchset - 1],
256 "uploader": {"name": "User Name"}}
257 return event
258
259 def getChangeRestoredEvent(self):
260 event = {"type": "change-restored",
261 "change": {"project": self.project,
262 "branch": self.branch,
263 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
264 "number": str(self.number),
265 "subject": self.subject,
266 "owner": {"name": "User Name"},
267 "url": "https://hostname/3"},
268 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100269 "patchSet": self.patchsets[-1],
270 "reason": ""}
271 return event
272
273 def getChangeAbandonedEvent(self):
274 event = {"type": "change-abandoned",
275 "change": {"project": self.project,
276 "branch": self.branch,
277 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
278 "number": str(self.number),
279 "subject": self.subject,
280 "owner": {"name": "User Name"},
281 "url": "https://hostname/3"},
282 "abandoner": {"name": "User Name"},
283 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700284 "reason": ""}
285 return event
286
287 def getChangeCommentEvent(self, patchset):
288 event = {"type": "comment-added",
289 "change": {"project": self.project,
290 "branch": self.branch,
291 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
292 "number": str(self.number),
293 "subject": self.subject,
294 "owner": {"name": "User Name"},
295 "url": "https://hostname/3"},
296 "patchSet": self.patchsets[patchset - 1],
297 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200298 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700299 "description": "Code-Review",
300 "value": "0"}],
301 "comment": "This is a comment"}
302 return event
303
James E. Blairc2a5ed72017-02-20 14:12:01 -0500304 def getChangeMergedEvent(self):
305 event = {"submitter": {"name": "Jenkins",
306 "username": "jenkins"},
307 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
308 "patchSet": self.patchsets[-1],
309 "change": self.data,
310 "type": "change-merged",
311 "eventCreatedOn": 1487613810}
312 return event
313
James E. Blair8cce42e2016-10-18 08:18:36 -0700314 def getRefUpdatedEvent(self):
315 path = os.path.join(self.upstream_root, self.project)
316 repo = git.Repo(path)
317 oldrev = repo.heads[self.branch].commit.hexsha
318
319 event = {
320 "type": "ref-updated",
321 "submitter": {
322 "name": "User Name",
323 },
324 "refUpdate": {
325 "oldRev": oldrev,
326 "newRev": self.patchsets[-1]['revision'],
327 "refName": self.branch,
328 "project": self.project,
329 }
330 }
331 return event
332
Joshua Hesketh642824b2014-07-01 17:54:59 +1000333 def addApproval(self, category, value, username='reviewer_john',
334 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700335 if not granted_on:
336 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000337 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200338 'description': self.categories[category][0],
339 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000340 'value': str(value),
341 'by': {
342 'username': username,
343 'email': username + '@example.com',
344 },
345 'grantedOn': int(granted_on)
346 }
Clark Boylanb640e052014-04-03 16:41:46 -0700347 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200348 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700349 del self.patchsets[-1]['approvals'][i]
350 self.patchsets[-1]['approvals'].append(approval)
351 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000352 'author': {'email': 'author@example.com',
353 'name': 'Patchset Author',
354 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700355 'change': {'branch': self.branch,
356 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
357 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000358 'owner': {'email': 'owner@example.com',
359 'name': 'Change Owner',
360 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700361 'project': self.project,
362 'subject': self.subject,
363 'topic': 'master',
364 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000365 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700366 'patchSet': self.patchsets[-1],
367 'type': 'comment-added'}
368 self.data['submitRecords'] = self.getSubmitRecords()
369 return json.loads(json.dumps(event))
370
371 def getSubmitRecords(self):
372 status = {}
373 for cat in self.categories.keys():
374 status[cat] = 0
375
376 for a in self.patchsets[-1]['approvals']:
377 cur = status[a['type']]
378 cat_min, cat_max = self.categories[a['type']][1:]
379 new = int(a['value'])
380 if new == cat_min:
381 cur = new
382 elif abs(new) > abs(cur):
383 cur = new
384 status[a['type']] = cur
385
386 labels = []
387 ok = True
388 for typ, cat in self.categories.items():
389 cur = status[typ]
390 cat_min, cat_max = cat[1:]
391 if cur == cat_min:
392 value = 'REJECT'
393 ok = False
394 elif cur == cat_max:
395 value = 'OK'
396 else:
397 value = 'NEED'
398 ok = False
399 labels.append({'label': cat[0], 'status': value})
400 if ok:
401 return [{'status': 'OK'}]
402 return [{'status': 'NOT_READY',
403 'labels': labels}]
404
405 def setDependsOn(self, other, patchset):
406 self.depends_on_change = other
407 d = {'id': other.data['id'],
408 'number': other.data['number'],
409 'ref': other.patchsets[patchset - 1]['ref']
410 }
411 self.data['dependsOn'] = [d]
412
413 other.needed_by_changes.append(self)
414 needed = other.data.get('neededBy', [])
415 d = {'id': self.data['id'],
416 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700417 'ref': self.patchsets[-1]['ref'],
418 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700419 }
420 needed.append(d)
421 other.data['neededBy'] = needed
422
423 def query(self):
424 self.queried += 1
425 d = self.data.get('dependsOn')
426 if d:
427 d = d[0]
428 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
429 d['isCurrentPatchSet'] = True
430 else:
431 d['isCurrentPatchSet'] = False
432 return json.loads(json.dumps(self.data))
433
434 def setMerged(self):
435 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000436 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700437 return
438 if self.fail_merge:
439 return
440 self.data['status'] = 'MERGED'
441 self.open = False
442
443 path = os.path.join(self.upstream_root, self.project)
444 repo = git.Repo(path)
445 repo.heads[self.branch].commit = \
446 repo.commit(self.patchsets[-1]['revision'])
447
448 def setReported(self):
449 self.reported += 1
450
451
James E. Blaire511d2f2016-12-08 15:22:26 -0800452class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700453 """A Fake Gerrit connection for use in tests.
454
455 This subclasses
456 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
457 ability for tests to add changes to the fake Gerrit it represents.
458 """
459
Joshua Hesketh352264b2015-08-11 23:42:08 +1000460 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700461
James E. Blaire511d2f2016-12-08 15:22:26 -0800462 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700463 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800464 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 connection_config)
466
Monty Taylorb934c1a2017-06-16 19:31:47 -0500467 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700468 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
469 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700471 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200472 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700473
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700474 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700475 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700476 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700478 c = FakeGerritChange(self, self.change_number, project, branch,
479 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700480 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700481 self.changes[self.change_number] = c
482 return c
483
Clark Boylanb640e052014-04-03 16:41:46 -0700484 def review(self, project, changeid, message, action):
485 number, ps = changeid.split(',')
486 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000487
488 # Add the approval back onto the change (ie simulate what gerrit would
489 # do).
490 # Usually when zuul leaves a review it'll create a feedback loop where
491 # zuul's review enters another gerrit event (which is then picked up by
492 # zuul). However, we can't mimic this behaviour (by adding this
493 # approval event into the queue) as it stops jobs from checking what
494 # happens before this event is triggered. If a job needs to see what
495 # happens they can add their own verified event into the queue.
496 # Nevertheless, we can update change with the new review in gerrit.
497
James E. Blair8b5408c2016-08-08 15:37:46 -0700498 for cat in action.keys():
499 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000500 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000501
Clark Boylanb640e052014-04-03 16:41:46 -0700502 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000503
Clark Boylanb640e052014-04-03 16:41:46 -0700504 if 'submit' in action:
505 change.setMerged()
506 if message:
507 change.setReported()
508
509 def query(self, number):
510 change = self.changes.get(int(number))
511 if change:
512 return change.query()
513 return {}
514
James E. Blairc494d542014-08-06 09:23:52 -0700515 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700516 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700517 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800518 if query.startswith('change:'):
519 # Query a specific changeid
520 changeid = query[len('change:'):]
521 l = [change.query() for change in self.changes.values()
522 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700523 elif query.startswith('message:'):
524 # Query the content of a commit message
525 msg = query[len('message:'):].strip()
526 l = [change.query() for change in self.changes.values()
527 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800528 else:
529 # Query all open changes
530 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700531 return l
James E. Blairc494d542014-08-06 09:23:52 -0700532
Joshua Hesketh352264b2015-08-11 23:42:08 +1000533 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700534 pass
535
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200536 def _uploadPack(self, project):
537 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
538 'multi_ack thin-pack side-band side-band-64k ofs-delta '
539 'shallow no-progress include-tag multi_ack_detailed no-done\n')
540 path = os.path.join(self.upstream_root, project.name)
541 repo = git.Repo(path)
542 for ref in repo.refs:
543 r = ref.object.hexsha + ' ' + ref.path + '\n'
544 ret += '%04x%s' % (len(r) + 4, r)
545 ret += '0000'
546 return ret
547
Joshua Hesketh352264b2015-08-11 23:42:08 +1000548 def getGitUrl(self, project):
549 return os.path.join(self.upstream_root, project.name)
550
Clark Boylanb640e052014-04-03 16:41:46 -0700551
Gregory Haynes4fc12542015-04-22 20:38:06 -0700552class GithubChangeReference(git.Reference):
553 _common_path_default = "refs/pull"
554 _points_to_commits_only = True
555
556
557class FakeGithubPullRequest(object):
558
559 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800560 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700561 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700562 """Creates a new PR with several commits.
563 Sends an event about opened PR."""
564 self.github = github
565 self.source = github
566 self.number = number
567 self.project = project
568 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100569 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700570 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100571 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700572 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100575 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100576 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800577 self.reviews = []
578 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 self.updated_at = None
580 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100581 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100582 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700583 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100585 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700586 self._updateTimeStamp()
587
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100590 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700591 self._updateTimeStamp()
592
Jan Hruban570d01c2016-03-10 21:51:32 +0100593 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700594 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100595 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700596 self._updateTimeStamp()
597
598 def getPullRequestOpenedEvent(self):
599 return self._getPullRequestEvent('opened')
600
601 def getPullRequestSynchronizeEvent(self):
602 return self._getPullRequestEvent('synchronize')
603
604 def getPullRequestReopenedEvent(self):
605 return self._getPullRequestEvent('reopened')
606
607 def getPullRequestClosedEvent(self):
608 return self._getPullRequestEvent('closed')
609
Jesse Keatinga41566f2017-06-14 18:17:51 -0700610 def getPullRequestEditedEvent(self):
611 return self._getPullRequestEvent('edited')
612
Gregory Haynes4fc12542015-04-22 20:38:06 -0700613 def addComment(self, message):
614 self.comments.append(message)
615 self._updateTimeStamp()
616
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200617 def getCommentAddedEvent(self, text):
618 name = 'issue_comment'
619 data = {
620 'action': 'created',
621 'issue': {
622 'number': self.number
623 },
624 'comment': {
625 'body': text
626 },
627 'repository': {
628 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100629 },
630 'sender': {
631 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200632 }
633 }
634 return (name, data)
635
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800636 def getReviewAddedEvent(self, review):
637 name = 'pull_request_review'
638 data = {
639 'action': 'submitted',
640 'pull_request': {
641 'number': self.number,
642 'title': self.subject,
643 'updated_at': self.updated_at,
644 'base': {
645 'ref': self.branch,
646 'repo': {
647 'full_name': self.project
648 }
649 },
650 'head': {
651 'sha': self.head_sha
652 }
653 },
654 'review': {
655 'state': review
656 },
657 'repository': {
658 'full_name': self.project
659 },
660 'sender': {
661 'login': 'ghuser'
662 }
663 }
664 return (name, data)
665
Jan Hruban16ad31f2015-11-07 14:39:07 +0100666 def addLabel(self, name):
667 if name not in self.labels:
668 self.labels.append(name)
669 self._updateTimeStamp()
670 return self._getLabelEvent(name)
671
672 def removeLabel(self, name):
673 if name in self.labels:
674 self.labels.remove(name)
675 self._updateTimeStamp()
676 return self._getUnlabelEvent(name)
677
678 def _getLabelEvent(self, label):
679 name = 'pull_request'
680 data = {
681 'action': 'labeled',
682 'pull_request': {
683 'number': self.number,
684 'updated_at': self.updated_at,
685 'base': {
686 'ref': self.branch,
687 'repo': {
688 'full_name': self.project
689 }
690 },
691 'head': {
692 'sha': self.head_sha
693 }
694 },
695 'label': {
696 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100697 },
698 'sender': {
699 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100700 }
701 }
702 return (name, data)
703
704 def _getUnlabelEvent(self, label):
705 name = 'pull_request'
706 data = {
707 'action': 'unlabeled',
708 'pull_request': {
709 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100710 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100711 'updated_at': self.updated_at,
712 'base': {
713 'ref': self.branch,
714 'repo': {
715 'full_name': self.project
716 }
717 },
718 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800719 'sha': self.head_sha,
720 'repo': {
721 'full_name': self.project
722 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100723 }
724 },
725 'label': {
726 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100727 },
728 'sender': {
729 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100730 }
731 }
732 return (name, data)
733
Jesse Keatinga41566f2017-06-14 18:17:51 -0700734 def editBody(self, body):
735 self.body = body
736 self._updateTimeStamp()
737
Gregory Haynes4fc12542015-04-22 20:38:06 -0700738 def _getRepo(self):
739 repo_path = os.path.join(self.upstream_root, self.project)
740 return git.Repo(repo_path)
741
742 def _createPRRef(self):
743 repo = self._getRepo()
744 GithubChangeReference.create(
745 repo, self._getPRReference(), 'refs/tags/init')
746
Jan Hruban570d01c2016-03-10 21:51:32 +0100747 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700748 repo = self._getRepo()
749 ref = repo.references[self._getPRReference()]
750 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100751 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700752 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100753 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700754 repo.head.reference = ref
755 zuul.merger.merger.reset_repo_to_head(repo)
756 repo.git.clean('-x', '-f', '-d')
757
Jan Hruban570d01c2016-03-10 21:51:32 +0100758 if files:
759 fn = files[0]
760 self.files = files
761 else:
762 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
763 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100764 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700765 fn = os.path.join(repo.working_dir, fn)
766 f = open(fn, 'w')
767 with open(fn, 'w') as f:
768 f.write("test %s %s\n" %
769 (self.branch, self.number))
770 repo.index.add([fn])
771
772 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800773 # Create an empty set of statuses for the given sha,
774 # each sha on a PR may have a status set on it
775 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700776 repo.head.reference = 'master'
777 zuul.merger.merger.reset_repo_to_head(repo)
778 repo.git.clean('-x', '-f', '-d')
779 repo.heads['master'].checkout()
780
781 def _updateTimeStamp(self):
782 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
783
784 def getPRHeadSha(self):
785 repo = self._getRepo()
786 return repo.references[self._getPRReference()].commit.hexsha
787
Jesse Keatingae4cd272017-01-30 17:10:44 -0800788 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800789 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
790 # convert the timestamp to a str format that would be returned
791 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800792
Adam Gandelmand81dd762017-02-09 15:15:49 -0800793 if granted_on:
794 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
795 submitted_at = time.strftime(
796 gh_time_format, granted_on.timetuple())
797 else:
798 # github timestamps only down to the second, so we need to make
799 # sure reviews that tests add appear to be added over a period of
800 # time in the past and not all at once.
801 if not self.reviews:
802 # the first review happens 10 mins ago
803 offset = 600
804 else:
805 # subsequent reviews happen 1 minute closer to now
806 offset = 600 - (len(self.reviews) * 60)
807
808 granted_on = datetime.datetime.utcfromtimestamp(
809 time.time() - offset)
810 submitted_at = time.strftime(
811 gh_time_format, granted_on.timetuple())
812
Jesse Keatingae4cd272017-01-30 17:10:44 -0800813 self.reviews.append({
814 'state': state,
815 'user': {
816 'login': user,
817 'email': user + "@derp.com",
818 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800819 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800820 })
821
Gregory Haynes4fc12542015-04-22 20:38:06 -0700822 def _getPRReference(self):
823 return '%s/head' % self.number
824
825 def _getPullRequestEvent(self, action):
826 name = 'pull_request'
827 data = {
828 'action': action,
829 'number': self.number,
830 'pull_request': {
831 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100832 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700833 'updated_at': self.updated_at,
834 'base': {
835 'ref': self.branch,
836 'repo': {
837 'full_name': self.project
838 }
839 },
840 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800841 'sha': self.head_sha,
842 'repo': {
843 'full_name': self.project
844 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700845 },
846 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100847 },
848 'sender': {
849 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700850 }
851 }
852 return (name, data)
853
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800854 def getCommitStatusEvent(self, context, state='success', user='zuul'):
855 name = 'status'
856 data = {
857 'state': state,
858 'sha': self.head_sha,
859 'description': 'Test results for %s: %s' % (self.head_sha, state),
860 'target_url': 'http://zuul/%s' % self.head_sha,
861 'branches': [],
862 'context': context,
863 'sender': {
864 'login': user
865 }
866 }
867 return (name, data)
868
James E. Blair289f5932017-07-27 15:02:29 -0700869 def setMerged(self, commit_message):
870 self.is_merged = True
871 self.merge_message = commit_message
872
873 repo = self._getRepo()
874 repo.heads[self.branch].commit = repo.commit(self.head_sha)
875
Gregory Haynes4fc12542015-04-22 20:38:06 -0700876
877class FakeGithubConnection(githubconnection.GithubConnection):
878 log = logging.getLogger("zuul.test.FakeGithubConnection")
879
880 def __init__(self, driver, connection_name, connection_config,
881 upstream_root=None):
882 super(FakeGithubConnection, self).__init__(driver, connection_name,
883 connection_config)
884 self.connection_name = connection_name
885 self.pr_number = 0
886 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700887 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700888 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100889 self.merge_failure = False
890 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100891 self.reports = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700892
Jesse Keatinga41566f2017-06-14 18:17:51 -0700893 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700894 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700895 self.pr_number += 1
896 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100897 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700898 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700899 self.pull_requests.append(pull_request)
900 return pull_request
901
Jesse Keating71a47ff2017-06-06 11:36:43 -0700902 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
903 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700904 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700905 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700906 if not new_rev:
907 new_rev = random_sha1()
908 name = 'push'
909 data = {
910 'ref': ref,
911 'before': old_rev,
912 'after': new_rev,
913 'repository': {
914 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700915 },
916 'commits': [
917 {
918 'added': added_files,
919 'removed': removed_files,
920 'modified': modified_files
921 }
922 ]
Wayne1a78c612015-06-11 17:14:13 -0700923 }
924 return (name, data)
925
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926 def emitEvent(self, event):
927 """Emulates sending the GitHub webhook event to the connection."""
928 port = self.webapp.server.socket.getsockname()[1]
929 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700930 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -0700931 secret = self.connection_config['webhook_token']
932 signature = githubconnection._sign_request(payload, secret)
933 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700934 req = urllib.request.Request(
935 'http://localhost:%s/connection/%s/payload'
936 % (port, self.connection_name),
937 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000938 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700939
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200940 def getPull(self, project, number):
941 pr = self.pull_requests[number - 1]
942 data = {
943 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100944 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200945 'updated_at': pr.updated_at,
946 'base': {
947 'repo': {
948 'full_name': pr.project
949 },
950 'ref': pr.branch,
951 },
Jan Hruban37615e52015-11-19 14:30:49 +0100952 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700953 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200954 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800955 'sha': pr.head_sha,
956 'repo': {
957 'full_name': pr.project
958 }
Jesse Keating61040e72017-06-08 15:08:27 -0700959 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700960 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700961 'labels': pr.labels,
962 'merged': pr.is_merged,
963 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200964 }
965 return data
966
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800967 def getPullBySha(self, sha):
968 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
969 if len(prs) > 1:
970 raise Exception('Multiple pulls found with head sha: %s' % sha)
971 pr = prs[0]
972 return self.getPull(pr.project, pr.number)
973
Jesse Keatingae4cd272017-01-30 17:10:44 -0800974 def _getPullReviews(self, owner, project, number):
975 pr = self.pull_requests[number - 1]
976 return pr.reviews
977
Jan Hruban3b415922016-02-03 13:10:22 +0100978 def getUser(self, login):
979 data = {
980 'username': login,
981 'name': 'Github User',
982 'email': 'github.user@example.com'
983 }
984 return data
985
Jesse Keatingae4cd272017-01-30 17:10:44 -0800986 def getRepoPermission(self, project, login):
987 owner, proj = project.split('/')
988 for pr in self.pull_requests:
989 pr_owner, pr_project = pr.project.split('/')
990 if (pr_owner == owner and proj == pr_project):
991 if login in pr.writers:
992 return 'write'
993 else:
994 return 'read'
995
Gregory Haynes4fc12542015-04-22 20:38:06 -0700996 def getGitUrl(self, project):
997 return os.path.join(self.upstream_root, str(project))
998
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200999 def real_getGitUrl(self, project):
1000 return super(FakeGithubConnection, self).getGitUrl(project)
1001
Gregory Haynes4fc12542015-04-22 20:38:06 -07001002 def getProjectBranches(self, project):
1003 """Masks getProjectBranches since we don't have a real github"""
1004
1005 # just returns master for now
1006 return ['master']
1007
Jan Hrubane252a732017-01-03 15:03:09 +01001008 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001009 # record that this got reported
1010 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001011 pull_request = self.pull_requests[pr_number - 1]
1012 pull_request.addComment(message)
1013
Jan Hruban3b415922016-02-03 13:10:22 +01001014 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001015 # record that this got reported
1016 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001017 pull_request = self.pull_requests[pr_number - 1]
1018 if self.merge_failure:
1019 raise Exception('Pull request was not merged')
1020 if self.merge_not_allowed_count > 0:
1021 self.merge_not_allowed_count -= 1
1022 raise MergeFailure('Merge was not successful due to mergeability'
1023 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001024 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001025
Jesse Keatingd96e5882017-01-19 13:55:50 -08001026 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001027 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001028
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001029 def setCommitStatus(self, project, sha, state, url='', description='',
1030 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001031 # record that this got reported
1032 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001033 # always insert a status to the front of the list, to represent
1034 # the last status provided for a commit.
1035 # Since we're bypassing github API, which would require a user, we
1036 # default the user as 'zuul' here.
1037 self.statuses.setdefault(project, {}).setdefault(sha, [])
1038 self.statuses[project][sha].insert(0, {
1039 'state': state,
1040 'url': url,
1041 'description': description,
1042 'context': context,
1043 'creator': {
1044 'login': user
1045 }
1046 })
Jan Hrubane252a732017-01-03 15:03:09 +01001047
Jan Hruban16ad31f2015-11-07 14:39:07 +01001048 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001049 # record that this got reported
1050 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001051 pull_request = self.pull_requests[pr_number - 1]
1052 pull_request.addLabel(label)
1053
1054 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001055 # record that this got reported
1056 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001057 pull_request = self.pull_requests[pr_number - 1]
1058 pull_request.removeLabel(label)
1059
Jesse Keatinga41566f2017-06-14 18:17:51 -07001060 def _getNeededByFromPR(self, change):
1061 prs = []
1062 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001063 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001064 change.number))
1065 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001066 if not pr.body:
1067 body = ''
1068 else:
1069 body = pr.body
1070 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001071 # Get our version of a pull so that it's a dict
1072 pull = self.getPull(pr.project, pr.number)
1073 prs.append(pull)
1074
1075 return prs
1076
Gregory Haynes4fc12542015-04-22 20:38:06 -07001077
Clark Boylanb640e052014-04-03 16:41:46 -07001078class BuildHistory(object):
1079 def __init__(self, **kw):
1080 self.__dict__.update(kw)
1081
1082 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001083 return ("<Completed build, result: %s name: %s uuid: %s "
1084 "changes: %s ref: %s>" %
1085 (self.result, self.name, self.uuid,
1086 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001087
1088
Clark Boylanb640e052014-04-03 16:41:46 -07001089class FakeStatsd(threading.Thread):
1090 def __init__(self):
1091 threading.Thread.__init__(self)
1092 self.daemon = True
1093 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1094 self.sock.bind(('', 0))
1095 self.port = self.sock.getsockname()[1]
1096 self.wake_read, self.wake_write = os.pipe()
1097 self.stats = []
1098
1099 def run(self):
1100 while True:
1101 poll = select.poll()
1102 poll.register(self.sock, select.POLLIN)
1103 poll.register(self.wake_read, select.POLLIN)
1104 ret = poll.poll()
1105 for (fd, event) in ret:
1106 if fd == self.sock.fileno():
1107 data = self.sock.recvfrom(1024)
1108 if not data:
1109 return
1110 self.stats.append(data[0])
1111 if fd == self.wake_read:
1112 return
1113
1114 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001115 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001116
1117
James E. Blaire1767bc2016-08-02 10:00:27 -07001118class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001119 log = logging.getLogger("zuul.test")
1120
Paul Belanger174a8272017-03-14 13:20:10 -04001121 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001123 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001124 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001125 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001126 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001127 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001128 # TODOv3(jeblair): self.node is really "the label of the node
1129 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001130 # keep using it like this, or we may end up exposing more of
1131 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001132 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001133 self.node = None
1134 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001135 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001136 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001137 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001138 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001139 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001140 self.wait_condition = threading.Condition()
1141 self.waiting = False
1142 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001143 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001144 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001145 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001146 items = self.parameters['zuul']['items']
1147 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1148 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001149
James E. Blair3158e282016-08-19 09:34:11 -07001150 def __repr__(self):
1151 waiting = ''
1152 if self.waiting:
1153 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001154 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1155 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001156
Clark Boylanb640e052014-04-03 16:41:46 -07001157 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001158 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001159 self.wait_condition.acquire()
1160 self.wait_condition.notify()
1161 self.waiting = False
1162 self.log.debug("Build %s released" % self.unique)
1163 self.wait_condition.release()
1164
1165 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001166 """Return whether this build is being held.
1167
1168 :returns: Whether the build is being held.
1169 :rtype: bool
1170 """
1171
Clark Boylanb640e052014-04-03 16:41:46 -07001172 self.wait_condition.acquire()
1173 if self.waiting:
1174 ret = True
1175 else:
1176 ret = False
1177 self.wait_condition.release()
1178 return ret
1179
1180 def _wait(self):
1181 self.wait_condition.acquire()
1182 self.waiting = True
1183 self.log.debug("Build %s waiting" % self.unique)
1184 self.wait_condition.wait()
1185 self.wait_condition.release()
1186
1187 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001188 self.log.debug('Running build %s' % self.unique)
1189
Paul Belanger174a8272017-03-14 13:20:10 -04001190 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001191 self.log.debug('Holding build %s' % self.unique)
1192 self._wait()
1193 self.log.debug("Build %s continuing" % self.unique)
1194
James E. Blair412fba82017-01-26 15:00:50 -08001195 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001196 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001197 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001198 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001199 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001200 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001201 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001202
James E. Blaire1767bc2016-08-02 10:00:27 -07001203 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001204
James E. Blaira5dba232016-08-08 15:53:24 -07001205 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001206 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001207 for change in changes:
1208 if self.hasChanges(change):
1209 return True
1210 return False
1211
James E. Blaire7b99a02016-08-05 14:27:34 -07001212 def hasChanges(self, *changes):
1213 """Return whether this build has certain changes in its git repos.
1214
1215 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001216 are expected to be present (in order) in the git repository of
1217 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001218
1219 :returns: Whether the build has the indicated changes.
1220 :rtype: bool
1221
1222 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001223 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001224 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001225 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001226 try:
1227 repo = git.Repo(path)
1228 except NoSuchPathError as e:
1229 self.log.debug('%s' % e)
1230 return False
James E. Blair247cab72017-07-20 16:52:36 -07001231 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001232 commit_message = '%s-1' % change.subject
1233 self.log.debug("Checking if build %s has changes; commit_message "
1234 "%s; repo_messages %s" % (self, commit_message,
1235 repo_messages))
1236 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001237 self.log.debug(" messages do not match")
1238 return False
1239 self.log.debug(" OK")
1240 return True
1241
James E. Blaird8af5422017-05-24 13:59:40 -07001242 def getWorkspaceRepos(self, projects):
1243 """Return workspace git repo objects for the listed projects
1244
1245 :arg list projects: A list of strings, each the canonical name
1246 of a project.
1247
1248 :returns: A dictionary of {name: repo} for every listed
1249 project.
1250 :rtype: dict
1251
1252 """
1253
1254 repos = {}
1255 for project in projects:
1256 path = os.path.join(self.jobdir.src_root, project)
1257 repo = git.Repo(path)
1258 repos[project] = repo
1259 return repos
1260
Clark Boylanb640e052014-04-03 16:41:46 -07001261
Paul Belanger174a8272017-03-14 13:20:10 -04001262class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1263 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001264
Paul Belanger174a8272017-03-14 13:20:10 -04001265 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001266 they will report that they have started but then pause until
1267 released before reporting completion. This attribute may be
1268 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001269 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001270 be explicitly released.
1271
1272 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001273 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001274 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001275 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001276 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001277 self.hold_jobs_in_build = False
1278 self.lock = threading.Lock()
1279 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001280 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001281 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001282 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001283
James E. Blaira5dba232016-08-08 15:53:24 -07001284 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001285 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001286
1287 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001288 :arg Change change: The :py:class:`~tests.base.FakeChange`
1289 instance which should cause the job to fail. This job
1290 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001291
1292 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001293 l = self.fail_tests.get(name, [])
1294 l.append(change)
1295 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001296
James E. Blair962220f2016-08-03 11:22:38 -07001297 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001298 """Release a held build.
1299
1300 :arg str regex: A regular expression which, if supplied, will
1301 cause only builds with matching names to be released. If
1302 not supplied, all builds will be released.
1303
1304 """
James E. Blair962220f2016-08-03 11:22:38 -07001305 builds = self.running_builds[:]
1306 self.log.debug("Releasing build %s (%s)" % (regex,
1307 len(self.running_builds)))
1308 for build in builds:
1309 if not regex or re.match(regex, build.name):
1310 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001311 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001312 build.release()
1313 else:
1314 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001315 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001316 self.log.debug("Done releasing builds %s (%s)" %
1317 (regex, len(self.running_builds)))
1318
Paul Belanger174a8272017-03-14 13:20:10 -04001319 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001320 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001321 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001322 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001323 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001324 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001325 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001326 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001327 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1328 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001329
1330 def stopJob(self, job):
1331 self.log.debug("handle stop")
1332 parameters = json.loads(job.arguments)
1333 uuid = parameters['uuid']
1334 for build in self.running_builds:
1335 if build.unique == uuid:
1336 build.aborted = True
1337 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001338 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001339
James E. Blaira002b032017-04-18 10:35:48 -07001340 def stop(self):
1341 for build in self.running_builds:
1342 build.release()
1343 super(RecordingExecutorServer, self).stop()
1344
Joshua Hesketh50c21782016-10-13 21:34:14 +11001345
Paul Belanger174a8272017-03-14 13:20:10 -04001346class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001347 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001348 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001349 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001350 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001351 if not commit: # merge conflict
1352 self.recordResult('MERGER_FAILURE')
1353 return commit
1354
1355 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001356 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001357 self.executor_server.lock.acquire()
1358 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001359 BuildHistory(name=build.name, result=result, changes=build.changes,
1360 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001361 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001362 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001363 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001364 )
Paul Belanger174a8272017-03-14 13:20:10 -04001365 self.executor_server.running_builds.remove(build)
1366 del self.executor_server.job_builds[self.job.unique]
1367 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001368
1369 def runPlaybooks(self, args):
1370 build = self.executor_server.job_builds[self.job.unique]
1371 build.jobdir = self.jobdir
1372
1373 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1374 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001375 return result
1376
James E. Blair74a82cf2017-07-12 17:23:08 -07001377 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001378 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001379
Paul Belanger174a8272017-03-14 13:20:10 -04001380 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001381 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001382 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001383 else:
1384 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001385 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001386
James E. Blairad8dca02017-02-21 11:48:32 -05001387 def getHostList(self, args):
1388 self.log.debug("hostlist")
1389 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001390 for host in hosts:
1391 host['host_vars']['ansible_connection'] = 'local'
1392
1393 hosts.append(dict(
1394 name='localhost',
1395 host_vars=dict(ansible_connection='local'),
1396 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001397 return hosts
1398
James E. Blairf5dbd002015-12-23 15:26:17 -08001399
Clark Boylanb640e052014-04-03 16:41:46 -07001400class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001401 """A Gearman server for use in tests.
1402
1403 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1404 added to the queue but will not be distributed to workers
1405 until released. This attribute may be changed at any time and
1406 will take effect for subsequently enqueued jobs, but
1407 previously held jobs will still need to be explicitly
1408 released.
1409
1410 """
1411
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001412 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001413 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001414 if use_ssl:
1415 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1416 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1417 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1418 else:
1419 ssl_ca = None
1420 ssl_cert = None
1421 ssl_key = None
1422
1423 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1424 ssl_cert=ssl_cert,
1425 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001426
1427 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001428 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1429 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001430 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001431 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001432 job.waiting = self.hold_jobs_in_queue
1433 else:
1434 job.waiting = False
1435 if job.waiting:
1436 continue
1437 if job.name in connection.functions:
1438 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001439 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001440 connection.related_jobs[job.handle] = job
1441 job.worker_connection = connection
1442 job.running = True
1443 return job
1444 return None
1445
1446 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001447 """Release a held job.
1448
1449 :arg str regex: A regular expression which, if supplied, will
1450 cause only jobs with matching names to be released. If
1451 not supplied, all jobs will be released.
1452 """
Clark Boylanb640e052014-04-03 16:41:46 -07001453 released = False
1454 qlen = (len(self.high_queue) + len(self.normal_queue) +
1455 len(self.low_queue))
1456 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1457 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001458 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001459 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001460 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001461 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001462 self.log.debug("releasing queued job %s" %
1463 job.unique)
1464 job.waiting = False
1465 released = True
1466 else:
1467 self.log.debug("not releasing queued job %s" %
1468 job.unique)
1469 if released:
1470 self.wakeConnections()
1471 qlen = (len(self.high_queue) + len(self.normal_queue) +
1472 len(self.low_queue))
1473 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1474
1475
1476class FakeSMTP(object):
1477 log = logging.getLogger('zuul.FakeSMTP')
1478
1479 def __init__(self, messages, server, port):
1480 self.server = server
1481 self.port = port
1482 self.messages = messages
1483
1484 def sendmail(self, from_email, to_email, msg):
1485 self.log.info("Sending email from %s, to %s, with msg %s" % (
1486 from_email, to_email, msg))
1487
1488 headers = msg.split('\n\n', 1)[0]
1489 body = msg.split('\n\n', 1)[1]
1490
1491 self.messages.append(dict(
1492 from_email=from_email,
1493 to_email=to_email,
1494 msg=msg,
1495 headers=headers,
1496 body=body,
1497 ))
1498
1499 return True
1500
1501 def quit(self):
1502 return True
1503
1504
James E. Blairdce6cea2016-12-20 16:45:32 -08001505class FakeNodepool(object):
1506 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001507 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001508
1509 log = logging.getLogger("zuul.test.FakeNodepool")
1510
1511 def __init__(self, host, port, chroot):
1512 self.client = kazoo.client.KazooClient(
1513 hosts='%s:%s%s' % (host, port, chroot))
1514 self.client.start()
1515 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001516 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001517 self.thread = threading.Thread(target=self.run)
1518 self.thread.daemon = True
1519 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001520 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001521
1522 def stop(self):
1523 self._running = False
1524 self.thread.join()
1525 self.client.stop()
1526 self.client.close()
1527
1528 def run(self):
1529 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001530 try:
1531 self._run()
1532 except Exception:
1533 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001534 time.sleep(0.1)
1535
1536 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001537 if self.paused:
1538 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001539 for req in self.getNodeRequests():
1540 self.fulfillRequest(req)
1541
1542 def getNodeRequests(self):
1543 try:
1544 reqids = self.client.get_children(self.REQUEST_ROOT)
1545 except kazoo.exceptions.NoNodeError:
1546 return []
1547 reqs = []
1548 for oid in sorted(reqids):
1549 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001550 try:
1551 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001552 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001553 data['_oid'] = oid
1554 reqs.append(data)
1555 except kazoo.exceptions.NoNodeError:
1556 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001557 return reqs
1558
James E. Blaire18d4602017-01-05 11:17:28 -08001559 def getNodes(self):
1560 try:
1561 nodeids = self.client.get_children(self.NODE_ROOT)
1562 except kazoo.exceptions.NoNodeError:
1563 return []
1564 nodes = []
1565 for oid in sorted(nodeids):
1566 path = self.NODE_ROOT + '/' + oid
1567 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001568 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001569 data['_oid'] = oid
1570 try:
1571 lockfiles = self.client.get_children(path + '/lock')
1572 except kazoo.exceptions.NoNodeError:
1573 lockfiles = []
1574 if lockfiles:
1575 data['_lock'] = True
1576 else:
1577 data['_lock'] = False
1578 nodes.append(data)
1579 return nodes
1580
James E. Blaira38c28e2017-01-04 10:33:20 -08001581 def makeNode(self, request_id, node_type):
1582 now = time.time()
1583 path = '/nodepool/nodes/'
1584 data = dict(type=node_type,
1585 provider='test-provider',
1586 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001587 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001588 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001589 public_ipv4='127.0.0.1',
1590 private_ipv4=None,
1591 public_ipv6=None,
1592 allocated_to=request_id,
1593 state='ready',
1594 state_time=now,
1595 created_time=now,
1596 updated_time=now,
1597 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001598 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001599 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001600 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001601 path = self.client.create(path, data,
1602 makepath=True,
1603 sequence=True)
1604 nodeid = path.split("/")[-1]
1605 return nodeid
1606
James E. Blair6ab79e02017-01-06 10:10:17 -08001607 def addFailRequest(self, request):
1608 self.fail_requests.add(request['_oid'])
1609
James E. Blairdce6cea2016-12-20 16:45:32 -08001610 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001611 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001612 return
1613 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001614 oid = request['_oid']
1615 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001616
James E. Blair6ab79e02017-01-06 10:10:17 -08001617 if oid in self.fail_requests:
1618 request['state'] = 'failed'
1619 else:
1620 request['state'] = 'fulfilled'
1621 nodes = []
1622 for node in request['node_types']:
1623 nodeid = self.makeNode(oid, node)
1624 nodes.append(nodeid)
1625 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001626
James E. Blaira38c28e2017-01-04 10:33:20 -08001627 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001628 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001629 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001630 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001631 try:
1632 self.client.set(path, data)
1633 except kazoo.exceptions.NoNodeError:
1634 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001635
1636
James E. Blair498059b2016-12-20 13:50:13 -08001637class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001638 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001639 super(ChrootedKazooFixture, self).__init__()
1640
1641 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1642 if ':' in zk_host:
1643 host, port = zk_host.split(':')
1644 else:
1645 host = zk_host
1646 port = None
1647
1648 self.zookeeper_host = host
1649
1650 if not port:
1651 self.zookeeper_port = 2181
1652 else:
1653 self.zookeeper_port = int(port)
1654
Clark Boylan621ec9a2017-04-07 17:41:33 -07001655 self.test_id = test_id
1656
James E. Blair498059b2016-12-20 13:50:13 -08001657 def _setUp(self):
1658 # Make sure the test chroot paths do not conflict
1659 random_bits = ''.join(random.choice(string.ascii_lowercase +
1660 string.ascii_uppercase)
1661 for x in range(8))
1662
Clark Boylan621ec9a2017-04-07 17:41:33 -07001663 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001664 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1665
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001666 self.addCleanup(self._cleanup)
1667
James E. Blair498059b2016-12-20 13:50:13 -08001668 # Ensure the chroot path exists and clean up any pre-existing znodes.
1669 _tmp_client = kazoo.client.KazooClient(
1670 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1671 _tmp_client.start()
1672
1673 if _tmp_client.exists(self.zookeeper_chroot):
1674 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1675
1676 _tmp_client.ensure_path(self.zookeeper_chroot)
1677 _tmp_client.stop()
1678 _tmp_client.close()
1679
James E. Blair498059b2016-12-20 13:50:13 -08001680 def _cleanup(self):
1681 '''Remove the chroot path.'''
1682 # Need a non-chroot'ed client to remove the chroot path
1683 _tmp_client = kazoo.client.KazooClient(
1684 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1685 _tmp_client.start()
1686 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1687 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001688 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001689
1690
Joshua Heskethd78b4482015-09-14 16:56:34 -06001691class MySQLSchemaFixture(fixtures.Fixture):
1692 def setUp(self):
1693 super(MySQLSchemaFixture, self).setUp()
1694
1695 random_bits = ''.join(random.choice(string.ascii_lowercase +
1696 string.ascii_uppercase)
1697 for x in range(8))
1698 self.name = '%s_%s' % (random_bits, os.getpid())
1699 self.passwd = uuid.uuid4().hex
1700 db = pymysql.connect(host="localhost",
1701 user="openstack_citest",
1702 passwd="openstack_citest",
1703 db="openstack_citest")
1704 cur = db.cursor()
1705 cur.execute("create database %s" % self.name)
1706 cur.execute(
1707 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1708 (self.name, self.name, self.passwd))
1709 cur.execute("flush privileges")
1710
1711 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1712 self.passwd,
1713 self.name)
1714 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1715 self.addCleanup(self.cleanup)
1716
1717 def cleanup(self):
1718 db = pymysql.connect(host="localhost",
1719 user="openstack_citest",
1720 passwd="openstack_citest",
1721 db="openstack_citest")
1722 cur = db.cursor()
1723 cur.execute("drop database %s" % self.name)
1724 cur.execute("drop user '%s'@'localhost'" % self.name)
1725 cur.execute("flush privileges")
1726
1727
Maru Newby3fe5f852015-01-13 04:22:14 +00001728class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001729 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001730 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001731
James E. Blair1c236df2017-02-01 14:07:24 -08001732 def attachLogs(self, *args):
1733 def reader():
1734 self._log_stream.seek(0)
1735 while True:
1736 x = self._log_stream.read(4096)
1737 if not x:
1738 break
1739 yield x.encode('utf8')
1740 content = testtools.content.content_from_reader(
1741 reader,
1742 testtools.content_type.UTF8_TEXT,
1743 False)
1744 self.addDetail('logging', content)
1745
Clark Boylanb640e052014-04-03 16:41:46 -07001746 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001747 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001748 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1749 try:
1750 test_timeout = int(test_timeout)
1751 except ValueError:
1752 # If timeout value is invalid do not set a timeout.
1753 test_timeout = 0
1754 if test_timeout > 0:
1755 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1756
1757 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1758 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1759 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1760 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1761 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1762 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1763 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1764 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1765 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1766 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001767 self._log_stream = StringIO()
1768 self.addOnException(self.attachLogs)
1769 else:
1770 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001771
James E. Blair73b41772017-05-22 13:22:55 -07001772 # NOTE(jeblair): this is temporary extra debugging to try to
1773 # track down a possible leak.
1774 orig_git_repo_init = git.Repo.__init__
1775
1776 def git_repo_init(myself, *args, **kw):
1777 orig_git_repo_init(myself, *args, **kw)
1778 self.log.debug("Created git repo 0x%x %s" %
1779 (id(myself), repr(myself)))
1780
1781 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1782 git_repo_init))
1783
James E. Blair1c236df2017-02-01 14:07:24 -08001784 handler = logging.StreamHandler(self._log_stream)
1785 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1786 '%(levelname)-8s %(message)s')
1787 handler.setFormatter(formatter)
1788
1789 logger = logging.getLogger()
1790 logger.setLevel(logging.DEBUG)
1791 logger.addHandler(handler)
1792
Clark Boylan3410d532017-04-25 12:35:29 -07001793 # Make sure we don't carry old handlers around in process state
1794 # which slows down test runs
1795 self.addCleanup(logger.removeHandler, handler)
1796 self.addCleanup(handler.close)
1797 self.addCleanup(handler.flush)
1798
James E. Blair1c236df2017-02-01 14:07:24 -08001799 # NOTE(notmorgan): Extract logging overrides for specific
1800 # libraries from the OS_LOG_DEFAULTS env and create loggers
1801 # for each. This is used to limit the output during test runs
1802 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001803 log_defaults_from_env = os.environ.get(
1804 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001805 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001806
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 if log_defaults_from_env:
1808 for default in log_defaults_from_env.split(','):
1809 try:
1810 name, level_str = default.split('=', 1)
1811 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001812 logger = logging.getLogger(name)
1813 logger.setLevel(level)
1814 logger.addHandler(handler)
1815 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001816 except ValueError:
1817 # NOTE(notmorgan): Invalid format of the log default,
1818 # skip and don't try and apply a logger for the
1819 # specified module
1820 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001821
Maru Newby3fe5f852015-01-13 04:22:14 +00001822
1823class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001824 """A test case with a functioning Zuul.
1825
1826 The following class variables are used during test setup and can
1827 be overidden by subclasses but are effectively read-only once a
1828 test method starts running:
1829
1830 :cvar str config_file: This points to the main zuul config file
1831 within the fixtures directory. Subclasses may override this
1832 to obtain a different behavior.
1833
1834 :cvar str tenant_config_file: This is the tenant config file
1835 (which specifies from what git repos the configuration should
1836 be loaded). It defaults to the value specified in
1837 `config_file` but can be overidden by subclasses to obtain a
1838 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001839 configuration. See also the :py:func:`simple_layout`
1840 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001841
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001842 :cvar bool create_project_keys: Indicates whether Zuul should
1843 auto-generate keys for each project, or whether the test
1844 infrastructure should insert dummy keys to save time during
1845 startup. Defaults to False.
1846
James E. Blaire7b99a02016-08-05 14:27:34 -07001847 The following are instance variables that are useful within test
1848 methods:
1849
1850 :ivar FakeGerritConnection fake_<connection>:
1851 A :py:class:`~tests.base.FakeGerritConnection` will be
1852 instantiated for each connection present in the config file
1853 and stored here. For instance, `fake_gerrit` will hold the
1854 FakeGerritConnection object for a connection named `gerrit`.
1855
1856 :ivar FakeGearmanServer gearman_server: An instance of
1857 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1858 server that all of the Zuul components in this test use to
1859 communicate with each other.
1860
Paul Belanger174a8272017-03-14 13:20:10 -04001861 :ivar RecordingExecutorServer executor_server: An instance of
1862 :py:class:`~tests.base.RecordingExecutorServer` which is the
1863 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001864
1865 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1866 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001867 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001868 list upon completion.
1869
1870 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1871 objects representing completed builds. They are appended to
1872 the list in the order they complete.
1873
1874 """
1875
James E. Blair83005782015-12-11 14:46:03 -08001876 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001877 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001878 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001879 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001880
1881 def _startMerger(self):
1882 self.merge_server = zuul.merger.server.MergeServer(self.config,
1883 self.connections)
1884 self.merge_server.start()
1885
Maru Newby3fe5f852015-01-13 04:22:14 +00001886 def setUp(self):
1887 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001888
1889 self.setupZK()
1890
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001891 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001892 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001893 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1894 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001895 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001896 tmp_root = tempfile.mkdtemp(
1897 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001898 self.test_root = os.path.join(tmp_root, "zuul-test")
1899 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001900 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001901 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001902 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001903 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1904 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001905
1906 if os.path.exists(self.test_root):
1907 shutil.rmtree(self.test_root)
1908 os.makedirs(self.test_root)
1909 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001910 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001911 os.makedirs(self.merger_state_root)
1912 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001913
1914 # Make per test copy of Configuration.
1915 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001916 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1917 if not os.path.exists(self.private_key_file):
1918 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1919 shutil.copy(src_private_key_file, self.private_key_file)
1920 shutil.copy('{}.pub'.format(src_private_key_file),
1921 '{}.pub'.format(self.private_key_file))
1922 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001923 self.config.set('scheduler', 'tenant_config',
1924 os.path.join(
1925 FIXTURE_DIR,
1926 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001927 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001928 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001929 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001930 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001931 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001932 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001933
Clark Boylanb640e052014-04-03 16:41:46 -07001934 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001935 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1936 # see: https://github.com/jsocol/pystatsd/issues/61
1937 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001938 os.environ['STATSD_PORT'] = str(self.statsd.port)
1939 self.statsd.start()
1940 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001941 importlib.reload(statsd)
1942 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001943
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001944 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001945
1946 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001947 self.log.info("Gearman server on port %s" %
1948 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001949 if self.use_ssl:
1950 self.log.info('SSL enabled for gearman')
1951 self.config.set(
1952 'gearman', 'ssl_ca',
1953 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1954 self.config.set(
1955 'gearman', 'ssl_cert',
1956 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1957 self.config.set(
1958 'gearman', 'ssl_key',
1959 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001960
James E. Blaire511d2f2016-12-08 15:22:26 -08001961 gerritsource.GerritSource.replication_timeout = 1.5
1962 gerritsource.GerritSource.replication_retry_interval = 0.5
1963 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001964
Joshua Hesketh352264b2015-08-11 23:42:08 +10001965 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001966
Jan Hruban7083edd2015-08-21 14:00:54 +02001967 self.webapp = zuul.webapp.WebApp(
1968 self.sched, port=0, listen_address='127.0.0.1')
1969
Jan Hruban6b71aff2015-10-22 16:58:08 +02001970 self.event_queues = [
1971 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001972 self.sched.trigger_event_queue,
1973 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001974 ]
1975
James E. Blairfef78942016-03-11 16:28:56 -08001976 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001977 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001978
Paul Belanger174a8272017-03-14 13:20:10 -04001979 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001980 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001981 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001982 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001983 _test_root=self.test_root,
1984 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001985 self.executor_server.start()
1986 self.history = self.executor_server.build_history
1987 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001988
Paul Belanger174a8272017-03-14 13:20:10 -04001989 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001990 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001991 self.merge_client = zuul.merger.client.MergeClient(
1992 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001993 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001994 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001995 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001996
James E. Blair0d5a36e2017-02-21 10:53:44 -05001997 self.fake_nodepool = FakeNodepool(
1998 self.zk_chroot_fixture.zookeeper_host,
1999 self.zk_chroot_fixture.zookeeper_port,
2000 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002001
Paul Belanger174a8272017-03-14 13:20:10 -04002002 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002003 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002004 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002005 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002006
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002007 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002008
2009 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002010 self.webapp.start()
2011 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002012 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002013 # Cleanups are run in reverse order
2014 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002015 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002016 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002017
James E. Blairb9c0d772017-03-03 14:34:49 -08002018 self.sched.reconfigure(self.config)
2019 self.sched.resume()
2020
Tobias Henkel7df274b2017-05-26 17:41:11 +02002021 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002022 # Set up gerrit related fakes
2023 # Set a changes database so multiple FakeGerrit's can report back to
2024 # a virtual canonical database given by the configured hostname
2025 self.gerrit_changes_dbs = {}
2026
2027 def getGerritConnection(driver, name, config):
2028 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2029 con = FakeGerritConnection(driver, name, config,
2030 changes_db=db,
2031 upstream_root=self.upstream_root)
2032 self.event_queues.append(con.event_queue)
2033 setattr(self, 'fake_' + name, con)
2034 return con
2035
2036 self.useFixture(fixtures.MonkeyPatch(
2037 'zuul.driver.gerrit.GerritDriver.getConnection',
2038 getGerritConnection))
2039
Gregory Haynes4fc12542015-04-22 20:38:06 -07002040 def getGithubConnection(driver, name, config):
2041 con = FakeGithubConnection(driver, name, config,
2042 upstream_root=self.upstream_root)
2043 setattr(self, 'fake_' + name, con)
2044 return con
2045
2046 self.useFixture(fixtures.MonkeyPatch(
2047 'zuul.driver.github.GithubDriver.getConnection',
2048 getGithubConnection))
2049
James E. Blaire511d2f2016-12-08 15:22:26 -08002050 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002051 # TODO(jhesketh): This should come from lib.connections for better
2052 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002053 # Register connections from the config
2054 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002055
Joshua Hesketh352264b2015-08-11 23:42:08 +10002056 def FakeSMTPFactory(*args, **kw):
2057 args = [self.smtp_messages] + list(args)
2058 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002059
Joshua Hesketh352264b2015-08-11 23:42:08 +10002060 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002061
James E. Blaire511d2f2016-12-08 15:22:26 -08002062 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002063 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002064 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002065
James E. Blair83005782015-12-11 14:46:03 -08002066 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002067 # This creates the per-test configuration object. It can be
2068 # overriden by subclasses, but should not need to be since it
2069 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002070 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002071 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002072
James E. Blair39840362017-06-23 20:34:02 +01002073 sections = ['zuul', 'scheduler', 'executor', 'merger']
2074 for section in sections:
2075 if not self.config.has_section(section):
2076 self.config.add_section(section)
2077
James E. Blair06cc3922017-04-19 10:08:10 -07002078 if not self.setupSimpleLayout():
2079 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002080 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002081 self.tenant_config_file)
2082 git_path = os.path.join(
2083 os.path.dirname(
2084 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2085 'git')
2086 if os.path.exists(git_path):
2087 for reponame in os.listdir(git_path):
2088 project = reponame.replace('_', '/')
2089 self.copyDirToRepo(project,
2090 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002091 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002092 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002093 self.setupAllProjectKeys()
2094
James E. Blair06cc3922017-04-19 10:08:10 -07002095 def setupSimpleLayout(self):
2096 # If the test method has been decorated with a simple_layout,
2097 # use that instead of the class tenant_config_file. Set up a
2098 # single config-project with the specified layout, and
2099 # initialize repos for all of the 'project' entries which
2100 # appear in the layout.
2101 test_name = self.id().split('.')[-1]
2102 test = getattr(self, test_name)
2103 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002104 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002105 else:
2106 return False
2107
James E. Blairb70e55a2017-04-19 12:57:02 -07002108 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002109 path = os.path.join(FIXTURE_DIR, path)
2110 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002111 data = f.read()
2112 layout = yaml.safe_load(data)
2113 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002114 untrusted_projects = []
2115 for item in layout:
2116 if 'project' in item:
2117 name = item['project']['name']
2118 untrusted_projects.append(name)
2119 self.init_repo(name)
2120 self.addCommitToRepo(name, 'initial commit',
2121 files={'README': ''},
2122 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002123 if 'job' in item:
2124 jobname = item['job']['name']
2125 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002126
2127 root = os.path.join(self.test_root, "config")
2128 if not os.path.exists(root):
2129 os.makedirs(root)
2130 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2131 config = [{'tenant':
2132 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002133 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002134 {'config-projects': ['common-config'],
2135 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002136 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002137 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002138 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002139 os.path.join(FIXTURE_DIR, f.name))
2140
2141 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002142 self.addCommitToRepo('common-config', 'add content from fixture',
2143 files, branch='master', tag='init')
2144
2145 return True
2146
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002147 def setupAllProjectKeys(self):
2148 if self.create_project_keys:
2149 return
2150
James E. Blair39840362017-06-23 20:34:02 +01002151 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002152 with open(os.path.join(FIXTURE_DIR, path)) as f:
2153 tenant_config = yaml.safe_load(f.read())
2154 for tenant in tenant_config:
2155 sources = tenant['tenant']['source']
2156 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002157 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002158 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002159 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002160 self.setupProjectKeys(source, project)
2161
2162 def setupProjectKeys(self, source, project):
2163 # Make sure we set up an RSA key for the project so that we
2164 # don't spend time generating one:
2165
James E. Blair6459db12017-06-29 14:57:20 -07002166 if isinstance(project, dict):
2167 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002168 key_root = os.path.join(self.state_root, 'keys')
2169 if not os.path.isdir(key_root):
2170 os.mkdir(key_root, 0o700)
2171 private_key_file = os.path.join(key_root, source, project + '.pem')
2172 private_key_dir = os.path.dirname(private_key_file)
2173 self.log.debug("Installing test keys for project %s at %s" % (
2174 project, private_key_file))
2175 if not os.path.isdir(private_key_dir):
2176 os.makedirs(private_key_dir)
2177 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2178 with open(private_key_file, 'w') as o:
2179 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002180
James E. Blair498059b2016-12-20 13:50:13 -08002181 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002182 self.zk_chroot_fixture = self.useFixture(
2183 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002184 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002185 self.zk_chroot_fixture.zookeeper_host,
2186 self.zk_chroot_fixture.zookeeper_port,
2187 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002188
James E. Blair96c6bf82016-01-15 16:20:40 -08002189 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002190 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002191
2192 files = {}
2193 for (dirpath, dirnames, filenames) in os.walk(source_path):
2194 for filename in filenames:
2195 test_tree_filepath = os.path.join(dirpath, filename)
2196 common_path = os.path.commonprefix([test_tree_filepath,
2197 source_path])
2198 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2199 with open(test_tree_filepath, 'r') as f:
2200 content = f.read()
2201 files[relative_filepath] = content
2202 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002203 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002204
James E. Blaire18d4602017-01-05 11:17:28 -08002205 def assertNodepoolState(self):
2206 # Make sure that there are no pending requests
2207
2208 requests = self.fake_nodepool.getNodeRequests()
2209 self.assertEqual(len(requests), 0)
2210
2211 nodes = self.fake_nodepool.getNodes()
2212 for node in nodes:
2213 self.assertFalse(node['_lock'], "Node %s is locked" %
2214 (node['_oid'],))
2215
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002216 def assertNoGeneratedKeys(self):
2217 # Make sure that Zuul did not generate any project keys
2218 # (unless it was supposed to).
2219
2220 if self.create_project_keys:
2221 return
2222
2223 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2224 test_key = i.read()
2225
2226 key_root = os.path.join(self.state_root, 'keys')
2227 for root, dirname, files in os.walk(key_root):
2228 for fn in files:
2229 with open(os.path.join(root, fn)) as f:
2230 self.assertEqual(test_key, f.read())
2231
Clark Boylanb640e052014-04-03 16:41:46 -07002232 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002233 self.log.debug("Assert final state")
2234 # Make sure no jobs are running
2235 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002236 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002237 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002238 gc.collect()
2239 for obj in gc.get_objects():
2240 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002241 self.log.debug("Leaked git repo object: 0x%x %s" %
2242 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002243 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002244 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002245 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002246 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002247 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002248 for tenant in self.sched.abide.tenants.values():
2249 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002250 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002251 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002252
2253 def shutdown(self):
2254 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002255 self.executor_server.hold_jobs_in_build = False
2256 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002257 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002258 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002259 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002260 self.sched.stop()
2261 self.sched.join()
2262 self.statsd.stop()
2263 self.statsd.join()
2264 self.webapp.stop()
2265 self.webapp.join()
2266 self.rpc.stop()
2267 self.rpc.join()
2268 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002269 self.fake_nodepool.stop()
2270 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002271 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002272 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002273 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002274 # Further the pydevd threads also need to be whitelisted so debugging
2275 # e.g. in PyCharm is possible without breaking shutdown.
2276 whitelist = ['executor-watchdog',
2277 'pydevd.CommandThread',
2278 'pydevd.Reader',
2279 'pydevd.Writer',
2280 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002281 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002282 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002283 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002284 log_str = ""
2285 for thread_id, stack_frame in sys._current_frames().items():
2286 log_str += "Thread: %s\n" % thread_id
2287 log_str += "".join(traceback.format_stack(stack_frame))
2288 self.log.debug(log_str)
2289 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002290
James E. Blaira002b032017-04-18 10:35:48 -07002291 def assertCleanShutdown(self):
2292 pass
2293
James E. Blairc4ba97a2017-04-19 16:26:24 -07002294 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002295 parts = project.split('/')
2296 path = os.path.join(self.upstream_root, *parts[:-1])
2297 if not os.path.exists(path):
2298 os.makedirs(path)
2299 path = os.path.join(self.upstream_root, project)
2300 repo = git.Repo.init(path)
2301
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002302 with repo.config_writer() as config_writer:
2303 config_writer.set_value('user', 'email', 'user@example.com')
2304 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002305
Clark Boylanb640e052014-04-03 16:41:46 -07002306 repo.index.commit('initial commit')
2307 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002308 if tag:
2309 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002310
James E. Blair97d902e2014-08-21 13:25:56 -07002311 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002312 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002313 repo.git.clean('-x', '-f', '-d')
2314
James E. Blair97d902e2014-08-21 13:25:56 -07002315 def create_branch(self, project, branch):
2316 path = os.path.join(self.upstream_root, project)
2317 repo = git.Repo.init(path)
2318 fn = os.path.join(path, 'README')
2319
2320 branch_head = repo.create_head(branch)
2321 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002322 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002323 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002324 f.close()
2325 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002326 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002327
James E. Blair97d902e2014-08-21 13:25:56 -07002328 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002329 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002330 repo.git.clean('-x', '-f', '-d')
2331
Sachi King9f16d522016-03-16 12:20:45 +11002332 def create_commit(self, project):
2333 path = os.path.join(self.upstream_root, project)
2334 repo = git.Repo(path)
2335 repo.head.reference = repo.heads['master']
2336 file_name = os.path.join(path, 'README')
2337 with open(file_name, 'a') as f:
2338 f.write('creating fake commit\n')
2339 repo.index.add([file_name])
2340 commit = repo.index.commit('Creating a fake commit')
2341 return commit.hexsha
2342
James E. Blairf4a5f022017-04-18 14:01:10 -07002343 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002344 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002345 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002346 while len(self.builds):
2347 self.release(self.builds[0])
2348 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002349 i += 1
2350 if count is not None and i >= count:
2351 break
James E. Blairb8c16472015-05-05 14:55:26 -07002352
James E. Blairdf25ddc2017-07-08 07:57:09 -07002353 def getSortedBuilds(self):
2354 "Return the list of currently running builds sorted by name"
2355
2356 return sorted(self.builds, key=lambda x: x.name)
2357
Clark Boylanb640e052014-04-03 16:41:46 -07002358 def release(self, job):
2359 if isinstance(job, FakeBuild):
2360 job.release()
2361 else:
2362 job.waiting = False
2363 self.log.debug("Queued job %s released" % job.unique)
2364 self.gearman_server.wakeConnections()
2365
2366 def getParameter(self, job, name):
2367 if isinstance(job, FakeBuild):
2368 return job.parameters[name]
2369 else:
2370 parameters = json.loads(job.arguments)
2371 return parameters[name]
2372
Clark Boylanb640e052014-04-03 16:41:46 -07002373 def haveAllBuildsReported(self):
2374 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002375 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002376 return False
2377 # Find out if every build that the worker has completed has been
2378 # reported back to Zuul. If it hasn't then that means a Gearman
2379 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002380 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002381 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002382 if not zbuild:
2383 # It has already been reported
2384 continue
2385 # It hasn't been reported yet.
2386 return False
2387 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002388 worker = self.executor_server.executor_worker
2389 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002390 if connection.state == 'GRAB_WAIT':
2391 return False
2392 return True
2393
2394 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002395 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002396 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002397 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002398 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002399 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002400 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002401 for j in conn.related_jobs.values():
2402 if j.unique == build.uuid:
2403 client_job = j
2404 break
2405 if not client_job:
2406 self.log.debug("%s is not known to the gearman client" %
2407 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002408 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002409 if not client_job.handle:
2410 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002411 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002412 server_job = self.gearman_server.jobs.get(client_job.handle)
2413 if not server_job:
2414 self.log.debug("%s is not known to the gearman server" %
2415 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002416 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002417 if not hasattr(server_job, 'waiting'):
2418 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002419 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002420 if server_job.waiting:
2421 continue
James E. Blair17302972016-08-10 16:11:42 -07002422 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002423 self.log.debug("%s has not reported start" % build)
2424 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002425 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002426 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002427 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002428 if worker_build:
2429 if worker_build.isWaiting():
2430 continue
2431 else:
2432 self.log.debug("%s is running" % worker_build)
2433 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002434 else:
James E. Blair962220f2016-08-03 11:22:38 -07002435 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002436 return False
James E. Blaira002b032017-04-18 10:35:48 -07002437 for (build_uuid, job_worker) in \
2438 self.executor_server.job_workers.items():
2439 if build_uuid not in seen_builds:
2440 self.log.debug("%s is not finalized" % build_uuid)
2441 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002442 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002443
James E. Blairdce6cea2016-12-20 16:45:32 -08002444 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002445 if self.fake_nodepool.paused:
2446 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002447 if self.sched.nodepool.requests:
2448 return False
2449 return True
2450
Jan Hruban6b71aff2015-10-22 16:58:08 +02002451 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002452 for event_queue in self.event_queues:
2453 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002454
2455 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002456 for event_queue in self.event_queues:
2457 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002458
Clark Boylanb640e052014-04-03 16:41:46 -07002459 def waitUntilSettled(self):
2460 self.log.debug("Waiting until settled...")
2461 start = time.time()
2462 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002463 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002464 self.log.error("Timeout waiting for Zuul to settle")
2465 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002466 for event_queue in self.event_queues:
2467 self.log.error(" %s: %s" %
2468 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002469 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002470 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002471 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002472 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002473 self.log.error("All requests completed: %s" %
2474 (self.areAllNodeRequestsComplete(),))
2475 self.log.error("Merge client jobs: %s" %
2476 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002477 raise Exception("Timeout waiting for Zuul to settle")
2478 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002479
Paul Belanger174a8272017-03-14 13:20:10 -04002480 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002481 # have all build states propogated to zuul?
2482 if self.haveAllBuildsReported():
2483 # Join ensures that the queue is empty _and_ events have been
2484 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002485 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002486 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002487 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002488 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002489 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002490 self.areAllNodeRequestsComplete() and
2491 all(self.eventQueuesEmpty())):
2492 # The queue empty check is placed at the end to
2493 # ensure that if a component adds an event between
2494 # when locked the run handler and checked that the
2495 # components were stable, we don't erroneously
2496 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002497 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002498 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002499 self.log.debug("...settled.")
2500 return
2501 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002502 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002503 self.sched.wake_event.wait(0.1)
2504
2505 def countJobResults(self, jobs, result):
2506 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002507 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002508
Monty Taylor0d926122017-05-24 08:07:56 -05002509 def getBuildByName(self, name):
2510 for build in self.builds:
2511 if build.name == name:
2512 return build
2513 raise Exception("Unable to find build %s" % name)
2514
James E. Blair96c6bf82016-01-15 16:20:40 -08002515 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002516 for job in self.history:
2517 if (job.name == name and
2518 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002519 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002520 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002521 raise Exception("Unable to find job %s in history" % name)
2522
2523 def assertEmptyQueues(self):
2524 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002525 for tenant in self.sched.abide.tenants.values():
2526 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002527 for pipeline_queue in pipeline.queues:
2528 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002529 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002530 pipeline.name, pipeline_queue.name,
2531 pipeline_queue.queue))
2532 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002533 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002534
2535 def assertReportedStat(self, key, value=None, kind=None):
2536 start = time.time()
2537 while time.time() < (start + 5):
2538 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002539 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002540 if key == k:
2541 if value is None and kind is None:
2542 return
2543 elif value:
2544 if value == v:
2545 return
2546 elif kind:
2547 if v.endswith('|' + kind):
2548 return
2549 time.sleep(0.1)
2550
Clark Boylanb640e052014-04-03 16:41:46 -07002551 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002552
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002553 def assertBuilds(self, builds):
2554 """Assert that the running builds are as described.
2555
2556 The list of running builds is examined and must match exactly
2557 the list of builds described by the input.
2558
2559 :arg list builds: A list of dictionaries. Each item in the
2560 list must match the corresponding build in the build
2561 history, and each element of the dictionary must match the
2562 corresponding attribute of the build.
2563
2564 """
James E. Blair3158e282016-08-19 09:34:11 -07002565 try:
2566 self.assertEqual(len(self.builds), len(builds))
2567 for i, d in enumerate(builds):
2568 for k, v in d.items():
2569 self.assertEqual(
2570 getattr(self.builds[i], k), v,
2571 "Element %i in builds does not match" % (i,))
2572 except Exception:
2573 for build in self.builds:
2574 self.log.error("Running build: %s" % build)
2575 else:
2576 self.log.error("No running builds")
2577 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002578
James E. Blairb536ecc2016-08-31 10:11:42 -07002579 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002580 """Assert that the completed builds are as described.
2581
2582 The list of completed builds is examined and must match
2583 exactly the list of builds described by the input.
2584
2585 :arg list history: A list of dictionaries. Each item in the
2586 list must match the corresponding build in the build
2587 history, and each element of the dictionary must match the
2588 corresponding attribute of the build.
2589
James E. Blairb536ecc2016-08-31 10:11:42 -07002590 :arg bool ordered: If true, the history must match the order
2591 supplied, if false, the builds are permitted to have
2592 arrived in any order.
2593
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002594 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002595 def matches(history_item, item):
2596 for k, v in item.items():
2597 if getattr(history_item, k) != v:
2598 return False
2599 return True
James E. Blair3158e282016-08-19 09:34:11 -07002600 try:
2601 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002602 if ordered:
2603 for i, d in enumerate(history):
2604 if not matches(self.history[i], d):
2605 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002606 "Element %i in history does not match %s" %
2607 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002608 else:
2609 unseen = self.history[:]
2610 for i, d in enumerate(history):
2611 found = False
2612 for unseen_item in unseen:
2613 if matches(unseen_item, d):
2614 found = True
2615 unseen.remove(unseen_item)
2616 break
2617 if not found:
2618 raise Exception("No match found for element %i "
2619 "in history" % (i,))
2620 if unseen:
2621 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002622 except Exception:
2623 for build in self.history:
2624 self.log.error("Completed build: %s" % build)
2625 else:
2626 self.log.error("No completed builds")
2627 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002628
James E. Blair6ac368c2016-12-22 18:07:20 -08002629 def printHistory(self):
2630 """Log the build history.
2631
2632 This can be useful during tests to summarize what jobs have
2633 completed.
2634
2635 """
2636 self.log.debug("Build history:")
2637 for build in self.history:
2638 self.log.debug(build)
2639
James E. Blair59fdbac2015-12-07 17:08:06 -08002640 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002641 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2642
James E. Blair9ea70072017-04-19 16:05:30 -07002643 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002644 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002645 if not os.path.exists(root):
2646 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002647 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2648 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002649- tenant:
2650 name: openstack
2651 source:
2652 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002653 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002654 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002655 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002656 - org/project
2657 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002658 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002659 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002660 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002661 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002662 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002663
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002664 def addCommitToRepo(self, project, message, files,
2665 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002666 path = os.path.join(self.upstream_root, project)
2667 repo = git.Repo(path)
2668 repo.head.reference = branch
2669 zuul.merger.merger.reset_repo_to_head(repo)
2670 for fn, content in files.items():
2671 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002672 try:
2673 os.makedirs(os.path.dirname(fn))
2674 except OSError:
2675 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002676 with open(fn, 'w') as f:
2677 f.write(content)
2678 repo.index.add([fn])
2679 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002680 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002681 repo.heads[branch].commit = commit
2682 repo.head.reference = branch
2683 repo.git.clean('-x', '-f', '-d')
2684 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002685 if tag:
2686 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002687 return before
2688
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002689 def commitConfigUpdate(self, project_name, source_name):
2690 """Commit an update to zuul.yaml
2691
2692 This overwrites the zuul.yaml in the specificed project with
2693 the contents specified.
2694
2695 :arg str project_name: The name of the project containing
2696 zuul.yaml (e.g., common-config)
2697
2698 :arg str source_name: The path to the file (underneath the
2699 test fixture directory) whose contents should be used to
2700 replace zuul.yaml.
2701 """
2702
2703 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002704 files = {}
2705 with open(source_path, 'r') as f:
2706 data = f.read()
2707 layout = yaml.safe_load(data)
2708 files['zuul.yaml'] = data
2709 for item in layout:
2710 if 'job' in item:
2711 jobname = item['job']['name']
2712 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002713 before = self.addCommitToRepo(
2714 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002715 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002716 return before
2717
James E. Blair7fc8daa2016-08-08 15:37:15 -07002718 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002719
James E. Blair7fc8daa2016-08-08 15:37:15 -07002720 """Inject a Fake (Gerrit) event.
2721
2722 This method accepts a JSON-encoded event and simulates Zuul
2723 having received it from Gerrit. It could (and should)
2724 eventually apply to any connection type, but is currently only
2725 used with Gerrit connections. The name of the connection is
2726 used to look up the corresponding server, and the event is
2727 simulated as having been received by all Zuul connections
2728 attached to that server. So if two Gerrit connections in Zuul
2729 are connected to the same Gerrit server, and you invoke this
2730 method specifying the name of one of them, the event will be
2731 received by both.
2732
2733 .. note::
2734
2735 "self.fake_gerrit.addEvent" calls should be migrated to
2736 this method.
2737
2738 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002739 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002740 :arg str event: The JSON-encoded event.
2741
2742 """
2743 specified_conn = self.connections.connections[connection]
2744 for conn in self.connections.connections.values():
2745 if (isinstance(conn, specified_conn.__class__) and
2746 specified_conn.server == conn.server):
2747 conn.addEvent(event)
2748
James E. Blaird8af5422017-05-24 13:59:40 -07002749 def getUpstreamRepos(self, projects):
2750 """Return upstream git repo objects for the listed projects
2751
2752 :arg list projects: A list of strings, each the canonical name
2753 of a project.
2754
2755 :returns: A dictionary of {name: repo} for every listed
2756 project.
2757 :rtype: dict
2758
2759 """
2760
2761 repos = {}
2762 for project in projects:
2763 # FIXME(jeblair): the upstream root does not yet have a
2764 # hostname component; that needs to be added, and this
2765 # line removed:
2766 tmp_project_name = '/'.join(project.split('/')[1:])
2767 path = os.path.join(self.upstream_root, tmp_project_name)
2768 repo = git.Repo(path)
2769 repos[project] = repo
2770 return repos
2771
James E. Blair3f876d52016-07-22 13:07:14 -07002772
2773class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002774 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002775 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002776
Jamie Lennox7655b552017-03-17 12:33:38 +11002777 @contextmanager
2778 def jobLog(self, build):
2779 """Print job logs on assertion errors
2780
2781 This method is a context manager which, if it encounters an
2782 ecxeption, adds the build log to the debug output.
2783
2784 :arg Build build: The build that's being asserted.
2785 """
2786 try:
2787 yield
2788 except Exception:
2789 path = os.path.join(self.test_root, build.uuid,
2790 'work', 'logs', 'job-output.txt')
2791 with open(path) as f:
2792 self.log.debug(f.read())
2793 raise
2794
Joshua Heskethd78b4482015-09-14 16:56:34 -06002795
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002796class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002797 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002798 use_ssl = True
2799
2800
Joshua Heskethd78b4482015-09-14 16:56:34 -06002801class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002802 def setup_config(self):
2803 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002804 for section_name in self.config.sections():
2805 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2806 section_name, re.I)
2807 if not con_match:
2808 continue
2809
2810 if self.config.get(section_name, 'driver') == 'sql':
2811 f = MySQLSchemaFixture()
2812 self.useFixture(f)
2813 if (self.config.get(section_name, 'dburi') ==
2814 '$MYSQL_FIXTURE_DBURI$'):
2815 self.config.set(section_name, 'dburi', f.dburi)