blob: e7151dfe787a15bd81c2ddcad514ecebf0c96033 [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 -050023from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043import git
44import gear
45import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080046import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080047import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060048import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070049import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blair6bacffb2018-01-05 13:45:25 -080055import tests.fakegithub
James E. Blaire511d2f2016-12-08 15:22:26 -080056import zuul.driver.gerrit.gerritsource as gerritsource
57import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070058import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.scheduler
60import zuul.webapp
Paul Belanger174a8272017-03-14 13:20:10 -040061import zuul.executor.server
62import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080063import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070065import zuul.merger.merger
66import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020067import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070068import zuul.nodepool
Jesse Keating80730e62017-09-14 15:35:11 -060069import zuul.rpcclient
James E. Blairdce6cea2016-12-20 16:45:32 -080070import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070071import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010072from zuul.exceptions import MergeFailure
Jesse Keating80730e62017-09-14 15:35:11 -060073from zuul.lib.config import get_default
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': [],
James E. Blair0e4c7912018-01-02 14:20:17 -0800175 'url': 'https://%s/%s' % (self.gerrit.server, number)}
Clark Boylanb640e052014-04-03 16:41:46 -0700176
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)
James E. Blair332636e2017-09-05 10:14:35 -0700198 if content is None:
199 os.unlink(fn)
200 repo.index.remove([fn])
201 else:
202 d = os.path.dirname(fn)
203 if not os.path.exists(d):
204 os.makedirs(d)
205 with open(fn, 'w') as f:
206 f.write(content)
207 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700208 else:
209 for fni in range(100):
210 fn = os.path.join(path, str(fni))
211 f = open(fn, 'w')
212 for ci in range(4096):
213 f.write(random.choice(string.printable))
214 f.close()
215 repo.index.add([fn])
216
217 r = repo.index.commit(msg)
218 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700219 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700220 repo.git.clean('-x', '-f', '-d')
221 repo.heads['master'].checkout()
222 return r
223
James E. Blair289f5932017-07-27 15:02:29 -0700224 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700225 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700227 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700228 data = ("test %s %s %s\n" %
229 (self.branch, self.number, self.latest_patchset))
230 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700231 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700232 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700233 ps_files = [{'file': '/COMMIT_MSG',
234 'type': 'ADDED'},
235 {'file': 'README',
236 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700237 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700238 ps_files.append({'file': f, 'type': 'ADDED'})
239 d = {'approvals': [],
240 'createdOn': time.time(),
241 'files': ps_files,
242 'number': str(self.latest_patchset),
243 'ref': 'refs/changes/1/%s/%s' % (self.number,
244 self.latest_patchset),
245 'revision': c.hexsha,
246 'uploader': {'email': 'user@example.com',
247 'name': 'User name',
248 'username': 'user'}}
249 self.data['currentPatchSet'] = d
250 self.patchsets.append(d)
251 self.data['submitRecords'] = self.getSubmitRecords()
252
253 def getPatchsetCreatedEvent(self, patchset):
254 event = {"type": "patchset-created",
255 "change": {"project": self.project,
256 "branch": self.branch,
257 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
258 "number": str(self.number),
259 "subject": self.subject,
260 "owner": {"name": "User Name"},
261 "url": "https://hostname/3"},
262 "patchSet": self.patchsets[patchset - 1],
263 "uploader": {"name": "User Name"}}
264 return event
265
266 def getChangeRestoredEvent(self):
267 event = {"type": "change-restored",
268 "change": {"project": self.project,
269 "branch": self.branch,
270 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
271 "number": str(self.number),
272 "subject": self.subject,
273 "owner": {"name": "User Name"},
274 "url": "https://hostname/3"},
275 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100276 "patchSet": self.patchsets[-1],
277 "reason": ""}
278 return event
279
280 def getChangeAbandonedEvent(self):
281 event = {"type": "change-abandoned",
282 "change": {"project": self.project,
283 "branch": self.branch,
284 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
285 "number": str(self.number),
286 "subject": self.subject,
287 "owner": {"name": "User Name"},
288 "url": "https://hostname/3"},
289 "abandoner": {"name": "User Name"},
290 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700291 "reason": ""}
292 return event
293
294 def getChangeCommentEvent(self, patchset):
295 event = {"type": "comment-added",
296 "change": {"project": self.project,
297 "branch": self.branch,
298 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
299 "number": str(self.number),
300 "subject": self.subject,
301 "owner": {"name": "User Name"},
302 "url": "https://hostname/3"},
303 "patchSet": self.patchsets[patchset - 1],
304 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200305 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700306 "description": "Code-Review",
307 "value": "0"}],
308 "comment": "This is a comment"}
309 return event
310
James E. Blairc2a5ed72017-02-20 14:12:01 -0500311 def getChangeMergedEvent(self):
312 event = {"submitter": {"name": "Jenkins",
313 "username": "jenkins"},
314 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
315 "patchSet": self.patchsets[-1],
316 "change": self.data,
317 "type": "change-merged",
318 "eventCreatedOn": 1487613810}
319 return event
320
James E. Blair8cce42e2016-10-18 08:18:36 -0700321 def getRefUpdatedEvent(self):
322 path = os.path.join(self.upstream_root, self.project)
323 repo = git.Repo(path)
324 oldrev = repo.heads[self.branch].commit.hexsha
325
326 event = {
327 "type": "ref-updated",
328 "submitter": {
329 "name": "User Name",
330 },
331 "refUpdate": {
332 "oldRev": oldrev,
333 "newRev": self.patchsets[-1]['revision'],
334 "refName": self.branch,
335 "project": self.project,
336 }
337 }
338 return event
339
Joshua Hesketh642824b2014-07-01 17:54:59 +1000340 def addApproval(self, category, value, username='reviewer_john',
341 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700342 if not granted_on:
343 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000344 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200345 'description': self.categories[category][0],
346 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000347 'value': str(value),
348 'by': {
349 'username': username,
350 'email': username + '@example.com',
351 },
352 'grantedOn': int(granted_on)
353 }
Clark Boylanb640e052014-04-03 16:41:46 -0700354 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200355 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700356 del self.patchsets[-1]['approvals'][i]
357 self.patchsets[-1]['approvals'].append(approval)
358 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000359 'author': {'email': 'author@example.com',
360 'name': 'Patchset Author',
361 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'change': {'branch': self.branch,
363 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
364 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000365 'owner': {'email': 'owner@example.com',
366 'name': 'Change Owner',
367 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700368 'project': self.project,
369 'subject': self.subject,
370 'topic': 'master',
371 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000372 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700373 'patchSet': self.patchsets[-1],
374 'type': 'comment-added'}
375 self.data['submitRecords'] = self.getSubmitRecords()
376 return json.loads(json.dumps(event))
377
378 def getSubmitRecords(self):
379 status = {}
380 for cat in self.categories.keys():
381 status[cat] = 0
382
383 for a in self.patchsets[-1]['approvals']:
384 cur = status[a['type']]
385 cat_min, cat_max = self.categories[a['type']][1:]
386 new = int(a['value'])
387 if new == cat_min:
388 cur = new
389 elif abs(new) > abs(cur):
390 cur = new
391 status[a['type']] = cur
392
393 labels = []
394 ok = True
395 for typ, cat in self.categories.items():
396 cur = status[typ]
397 cat_min, cat_max = cat[1:]
398 if cur == cat_min:
399 value = 'REJECT'
400 ok = False
401 elif cur == cat_max:
402 value = 'OK'
403 else:
404 value = 'NEED'
405 ok = False
406 labels.append({'label': cat[0], 'status': value})
407 if ok:
408 return [{'status': 'OK'}]
409 return [{'status': 'NOT_READY',
410 'labels': labels}]
411
412 def setDependsOn(self, other, patchset):
413 self.depends_on_change = other
414 d = {'id': other.data['id'],
415 'number': other.data['number'],
416 'ref': other.patchsets[patchset - 1]['ref']
417 }
418 self.data['dependsOn'] = [d]
419
420 other.needed_by_changes.append(self)
421 needed = other.data.get('neededBy', [])
422 d = {'id': self.data['id'],
423 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700424 'ref': self.patchsets[-1]['ref'],
425 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700426 }
427 needed.append(d)
428 other.data['neededBy'] = needed
429
430 def query(self):
431 self.queried += 1
432 d = self.data.get('dependsOn')
433 if d:
434 d = d[0]
435 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
436 d['isCurrentPatchSet'] = True
437 else:
438 d['isCurrentPatchSet'] = False
439 return json.loads(json.dumps(self.data))
440
441 def setMerged(self):
442 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000443 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700444 return
445 if self.fail_merge:
446 return
447 self.data['status'] = 'MERGED'
448 self.open = False
449
450 path = os.path.join(self.upstream_root, self.project)
451 repo = git.Repo(path)
452 repo.heads[self.branch].commit = \
453 repo.commit(self.patchsets[-1]['revision'])
454
455 def setReported(self):
456 self.reported += 1
457
458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700460 """A Fake Gerrit connection for use in tests.
461
462 This subclasses
463 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
464 ability for tests to add changes to the fake Gerrit it represents.
465 """
466
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700468
James E. Blaire511d2f2016-12-08 15:22:26 -0800469 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700470 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800471 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000472 connection_config)
473
Monty Taylorb934c1a2017-06-16 19:31:47 -0500474 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700475 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
476 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700478 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200479 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700480
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700481 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700482 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700483 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700484 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700485 c = FakeGerritChange(self, self.change_number, project, branch,
486 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700487 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700488 self.changes[self.change_number] = c
489 return c
490
James E. Blair1edfd972017-12-01 15:54:24 -0800491 def addFakeTag(self, project, branch, tag):
492 path = os.path.join(self.upstream_root, project)
493 repo = git.Repo(path)
494 commit = repo.heads[branch].commit
495 newrev = commit.hexsha
496 ref = 'refs/tags/' + tag
497
498 git.Tag.create(repo, tag, commit)
499
500 event = {
501 "type": "ref-updated",
502 "submitter": {
503 "name": "User Name",
504 },
505 "refUpdate": {
506 "oldRev": 40 * '0',
507 "newRev": newrev,
508 "refName": ref,
509 "project": project,
510 }
511 }
512 return event
513
James E. Blair72facdc2017-08-17 10:29:12 -0700514 def getFakeBranchCreatedEvent(self, project, branch):
515 path = os.path.join(self.upstream_root, project)
516 repo = git.Repo(path)
517 oldrev = 40 * '0'
518
519 event = {
520 "type": "ref-updated",
521 "submitter": {
522 "name": "User Name",
523 },
524 "refUpdate": {
525 "oldRev": oldrev,
526 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700527 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700528 "project": project,
529 }
530 }
531 return event
532
Clark Boylanb640e052014-04-03 16:41:46 -0700533 def review(self, project, changeid, message, action):
534 number, ps = changeid.split(',')
535 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000536
537 # Add the approval back onto the change (ie simulate what gerrit would
538 # do).
539 # Usually when zuul leaves a review it'll create a feedback loop where
540 # zuul's review enters another gerrit event (which is then picked up by
541 # zuul). However, we can't mimic this behaviour (by adding this
542 # approval event into the queue) as it stops jobs from checking what
543 # happens before this event is triggered. If a job needs to see what
544 # happens they can add their own verified event into the queue.
545 # Nevertheless, we can update change with the new review in gerrit.
546
James E. Blair8b5408c2016-08-08 15:37:46 -0700547 for cat in action.keys():
548 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000549 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000550
Clark Boylanb640e052014-04-03 16:41:46 -0700551 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000552
Clark Boylanb640e052014-04-03 16:41:46 -0700553 if 'submit' in action:
554 change.setMerged()
555 if message:
556 change.setReported()
557
558 def query(self, number):
559 change = self.changes.get(int(number))
560 if change:
561 return change.query()
562 return {}
563
James E. Blair0e4c7912018-01-02 14:20:17 -0800564 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800565 if query.startswith('change:'):
566 # Query a specific changeid
567 changeid = query[len('change:'):]
568 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800569 if (change.data['id'] == changeid or
570 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700571 elif query.startswith('message:'):
572 # Query the content of a commit message
573 msg = query[len('message:'):].strip()
574 l = [change.query() for change in self.changes.values()
575 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800576 else:
577 # Query all open changes
578 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700579 return l
James E. Blairc494d542014-08-06 09:23:52 -0700580
James E. Blair0e4c7912018-01-02 14:20:17 -0800581 def simpleQuery(self, query):
582 self.log.debug("simpleQuery: %s" % query)
583 self.queries.append(query)
584 results = []
585 if query.startswith('(') and 'OR' in query:
586 query = query[1:-2]
587 for q in query.split(' OR '):
588 for r in self._simpleQuery(q):
589 if r not in results:
590 results.append(r)
591 else:
592 results = self._simpleQuery(query)
593 return results
594
Joshua Hesketh352264b2015-08-11 23:42:08 +1000595 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700596 pass
597
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200598 def _uploadPack(self, project):
599 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
600 'multi_ack thin-pack side-band side-band-64k ofs-delta '
601 'shallow no-progress include-tag multi_ack_detailed no-done\n')
602 path = os.path.join(self.upstream_root, project.name)
603 repo = git.Repo(path)
604 for ref in repo.refs:
605 r = ref.object.hexsha + ' ' + ref.path + '\n'
606 ret += '%04x%s' % (len(r) + 4, r)
607 ret += '0000'
608 return ret
609
Joshua Hesketh352264b2015-08-11 23:42:08 +1000610 def getGitUrl(self, project):
611 return os.path.join(self.upstream_root, project.name)
612
Clark Boylanb640e052014-04-03 16:41:46 -0700613
Gregory Haynes4fc12542015-04-22 20:38:06 -0700614class GithubChangeReference(git.Reference):
615 _common_path_default = "refs/pull"
616 _points_to_commits_only = True
617
618
619class FakeGithubPullRequest(object):
620
621 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800622 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700623 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700624 """Creates a new PR with several commits.
625 Sends an event about opened PR."""
626 self.github = github
627 self.source = github
628 self.number = number
629 self.project = project
630 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100631 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700632 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100633 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700634 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100635 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700636 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100637 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100638 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800639 self.reviews = []
640 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700641 self.updated_at = None
642 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100643 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100644 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700645 self.state = 'open'
James E. Blair54145e02018-01-10 16:07:41 -0800646 self.url = 'https://%s/%s/pull/%s' % (github.server, project, number)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700647 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100648 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700649 self._updateTimeStamp()
650
Jan Hruban570d01c2016-03-10 21:51:32 +0100651 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700652 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100653 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700654 self._updateTimeStamp()
655
Jan Hruban570d01c2016-03-10 21:51:32 +0100656 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700657 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100658 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700659 self._updateTimeStamp()
660
661 def getPullRequestOpenedEvent(self):
662 return self._getPullRequestEvent('opened')
663
664 def getPullRequestSynchronizeEvent(self):
665 return self._getPullRequestEvent('synchronize')
666
667 def getPullRequestReopenedEvent(self):
668 return self._getPullRequestEvent('reopened')
669
670 def getPullRequestClosedEvent(self):
671 return self._getPullRequestEvent('closed')
672
Jesse Keatinga41566f2017-06-14 18:17:51 -0700673 def getPullRequestEditedEvent(self):
674 return self._getPullRequestEvent('edited')
675
Gregory Haynes4fc12542015-04-22 20:38:06 -0700676 def addComment(self, message):
677 self.comments.append(message)
678 self._updateTimeStamp()
679
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200680 def getCommentAddedEvent(self, text):
681 name = 'issue_comment'
682 data = {
683 'action': 'created',
684 'issue': {
685 'number': self.number
686 },
687 'comment': {
688 'body': text
689 },
690 'repository': {
691 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100692 },
693 'sender': {
694 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200695 }
696 }
697 return (name, data)
698
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800699 def getReviewAddedEvent(self, review):
700 name = 'pull_request_review'
701 data = {
702 'action': 'submitted',
703 'pull_request': {
704 'number': self.number,
705 'title': self.subject,
706 'updated_at': self.updated_at,
707 'base': {
708 'ref': self.branch,
709 'repo': {
710 'full_name': self.project
711 }
712 },
713 'head': {
714 'sha': self.head_sha
715 }
716 },
717 'review': {
718 'state': review
719 },
720 'repository': {
721 'full_name': self.project
722 },
723 'sender': {
724 'login': 'ghuser'
725 }
726 }
727 return (name, data)
728
Jan Hruban16ad31f2015-11-07 14:39:07 +0100729 def addLabel(self, name):
730 if name not in self.labels:
731 self.labels.append(name)
732 self._updateTimeStamp()
733 return self._getLabelEvent(name)
734
735 def removeLabel(self, name):
736 if name in self.labels:
737 self.labels.remove(name)
738 self._updateTimeStamp()
739 return self._getUnlabelEvent(name)
740
741 def _getLabelEvent(self, label):
742 name = 'pull_request'
743 data = {
744 'action': 'labeled',
745 'pull_request': {
746 'number': self.number,
747 'updated_at': self.updated_at,
748 'base': {
749 'ref': self.branch,
750 'repo': {
751 'full_name': self.project
752 }
753 },
754 'head': {
755 'sha': self.head_sha
756 }
757 },
758 'label': {
759 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100760 },
761 'sender': {
762 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100763 }
764 }
765 return (name, data)
766
767 def _getUnlabelEvent(self, label):
768 name = 'pull_request'
769 data = {
770 'action': 'unlabeled',
771 'pull_request': {
772 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100773 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100774 'updated_at': self.updated_at,
775 'base': {
776 'ref': self.branch,
777 'repo': {
778 'full_name': self.project
779 }
780 },
781 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800782 'sha': self.head_sha,
783 'repo': {
784 'full_name': self.project
785 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100786 }
787 },
788 'label': {
789 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100790 },
791 'sender': {
792 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100793 }
794 }
795 return (name, data)
796
Jesse Keatinga41566f2017-06-14 18:17:51 -0700797 def editBody(self, body):
798 self.body = body
799 self._updateTimeStamp()
800
Gregory Haynes4fc12542015-04-22 20:38:06 -0700801 def _getRepo(self):
802 repo_path = os.path.join(self.upstream_root, self.project)
803 return git.Repo(repo_path)
804
805 def _createPRRef(self):
806 repo = self._getRepo()
807 GithubChangeReference.create(
808 repo, self._getPRReference(), 'refs/tags/init')
809
Jan Hruban570d01c2016-03-10 21:51:32 +0100810 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700811 repo = self._getRepo()
812 ref = repo.references[self._getPRReference()]
813 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100814 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700815 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100816 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700817 repo.head.reference = ref
818 zuul.merger.merger.reset_repo_to_head(repo)
819 repo.git.clean('-x', '-f', '-d')
820
Jan Hruban570d01c2016-03-10 21:51:32 +0100821 if files:
822 fn = files[0]
823 self.files = files
824 else:
825 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
826 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100827 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700828 fn = os.path.join(repo.working_dir, fn)
829 f = open(fn, 'w')
830 with open(fn, 'w') as f:
831 f.write("test %s %s\n" %
832 (self.branch, self.number))
833 repo.index.add([fn])
834
835 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800836 # Create an empty set of statuses for the given sha,
837 # each sha on a PR may have a status set on it
838 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 repo.head.reference = 'master'
840 zuul.merger.merger.reset_repo_to_head(repo)
841 repo.git.clean('-x', '-f', '-d')
842 repo.heads['master'].checkout()
843
844 def _updateTimeStamp(self):
845 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
846
847 def getPRHeadSha(self):
848 repo = self._getRepo()
849 return repo.references[self._getPRReference()].commit.hexsha
850
Jesse Keatingae4cd272017-01-30 17:10:44 -0800851 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800852 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
853 # convert the timestamp to a str format that would be returned
854 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800855
Adam Gandelmand81dd762017-02-09 15:15:49 -0800856 if granted_on:
857 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
858 submitted_at = time.strftime(
859 gh_time_format, granted_on.timetuple())
860 else:
861 # github timestamps only down to the second, so we need to make
862 # sure reviews that tests add appear to be added over a period of
863 # time in the past and not all at once.
864 if not self.reviews:
865 # the first review happens 10 mins ago
866 offset = 600
867 else:
868 # subsequent reviews happen 1 minute closer to now
869 offset = 600 - (len(self.reviews) * 60)
870
871 granted_on = datetime.datetime.utcfromtimestamp(
872 time.time() - offset)
873 submitted_at = time.strftime(
874 gh_time_format, granted_on.timetuple())
875
Jesse Keatingae4cd272017-01-30 17:10:44 -0800876 self.reviews.append({
877 'state': state,
878 'user': {
879 'login': user,
880 'email': user + "@derp.com",
881 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800882 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800883 })
884
Gregory Haynes4fc12542015-04-22 20:38:06 -0700885 def _getPRReference(self):
886 return '%s/head' % self.number
887
888 def _getPullRequestEvent(self, action):
889 name = 'pull_request'
890 data = {
891 'action': action,
892 'number': self.number,
893 'pull_request': {
894 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100895 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700896 'updated_at': self.updated_at,
897 'base': {
898 'ref': self.branch,
899 'repo': {
900 'full_name': self.project
901 }
902 },
903 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800904 'sha': self.head_sha,
905 'repo': {
906 'full_name': self.project
907 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700908 },
909 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100910 },
911 'sender': {
912 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700913 }
914 }
915 return (name, data)
916
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800917 def getCommitStatusEvent(self, context, state='success', user='zuul'):
918 name = 'status'
919 data = {
920 'state': state,
921 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700922 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800923 'description': 'Test results for %s: %s' % (self.head_sha, state),
924 'target_url': 'http://zuul/%s' % self.head_sha,
925 'branches': [],
926 'context': context,
927 'sender': {
928 'login': user
929 }
930 }
931 return (name, data)
932
James E. Blair289f5932017-07-27 15:02:29 -0700933 def setMerged(self, commit_message):
934 self.is_merged = True
935 self.merge_message = commit_message
936
937 repo = self._getRepo()
938 repo.heads[self.branch].commit = repo.commit(self.head_sha)
939
Gregory Haynes4fc12542015-04-22 20:38:06 -0700940
941class FakeGithubConnection(githubconnection.GithubConnection):
942 log = logging.getLogger("zuul.test.FakeGithubConnection")
943
Jesse Keating80730e62017-09-14 15:35:11 -0600944 def __init__(self, driver, connection_name, connection_config, rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -0800945 changes_db=None, upstream_root=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700946 super(FakeGithubConnection, self).__init__(driver, connection_name,
947 connection_config)
948 self.connection_name = connection_name
949 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800950 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700951 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700952 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100953 self.merge_failure = False
954 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100955 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800956 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Jesse Keating80730e62017-09-14 15:35:11 -0600957 self.rpcclient = rpcclient
Tobias Henkel64e37a02017-08-02 10:13:30 +0200958
959 def getGithubClient(self,
960 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600961 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200962 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700963
Jesse Keating80730e62017-09-14 15:35:11 -0600964 def setZuulWebPort(self, port):
965 self.zuul_web_port = port
966
Jesse Keatinga41566f2017-06-14 18:17:51 -0700967 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700968 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700969 self.pr_number += 1
970 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100971 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700972 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800973 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700974 return pull_request
975
Jesse Keating71a47ff2017-06-06 11:36:43 -0700976 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
977 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700978 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700979 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700980 if not new_rev:
981 new_rev = random_sha1()
982 name = 'push'
983 data = {
984 'ref': ref,
985 'before': old_rev,
986 'after': new_rev,
987 'repository': {
988 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700989 },
990 'commits': [
991 {
992 'added': added_files,
993 'removed': removed_files,
994 'modified': modified_files
995 }
996 ]
Wayne1a78c612015-06-11 17:14:13 -0700997 }
998 return (name, data)
999
Jesse Keating80730e62017-09-14 15:35:11 -06001000 def emitEvent(self, event, use_zuulweb=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001001 """Emulates sending the GitHub webhook event to the connection."""
Gregory Haynes4fc12542015-04-22 20:38:06 -07001002 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001003 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001004 secret = self.connection_config['webhook_token']
1005 signature = githubconnection._sign_request(payload, secret)
Jesse Keating80730e62017-09-14 15:35:11 -06001006 headers = {'x-github-event': name, 'x-hub-signature': signature}
1007
1008 if use_zuulweb:
1009 req = urllib.request.Request(
1010 'http://127.0.0.1:%s/driver/github/%s/payload'
1011 % (self.zuul_web_port, self.connection_name),
1012 data=payload, headers=headers)
1013 return urllib.request.urlopen(req)
1014 else:
1015 job = self.rpcclient.submitJob(
1016 'github:%s:payload' % self.connection_name,
1017 {'headers': headers, 'body': data})
1018 return json.loads(job.data[0])
Gregory Haynes4fc12542015-04-22 20:38:06 -07001019
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001020 def addProject(self, project):
1021 # use the original method here and additionally register it in the
1022 # fake github
1023 super(FakeGithubConnection, self).addProject(project)
1024 self.getGithubClient(project).addProject(project)
1025
Jesse Keating9021a012017-08-29 14:45:27 -07001026 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001027 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001028 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001029 if len(prs) > 1:
1030 raise Exception('Multiple pulls found with head sha: %s' % sha)
1031 pr = prs[0]
1032 return self.getPull(pr.project, pr.number)
1033
Jesse Keatingae4cd272017-01-30 17:10:44 -08001034 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001035 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001036 return pr.reviews
1037
Jesse Keatingae4cd272017-01-30 17:10:44 -08001038 def getRepoPermission(self, project, login):
1039 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001040 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001041 pr_owner, pr_project = pr.project.split('/')
1042 if (pr_owner == owner and proj == pr_project):
1043 if login in pr.writers:
1044 return 'write'
1045 else:
1046 return 'read'
1047
Gregory Haynes4fc12542015-04-22 20:38:06 -07001048 def getGitUrl(self, project):
1049 return os.path.join(self.upstream_root, str(project))
1050
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001051 def real_getGitUrl(self, project):
1052 return super(FakeGithubConnection, self).getGitUrl(project)
1053
Jan Hrubane252a732017-01-03 15:03:09 +01001054 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001055 # record that this got reported
1056 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001057 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001058 pull_request.addComment(message)
1059
Jan Hruban3b415922016-02-03 13:10:22 +01001060 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001061 # record that this got reported
1062 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001063 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001064 if self.merge_failure:
1065 raise Exception('Pull request was not merged')
1066 if self.merge_not_allowed_count > 0:
1067 self.merge_not_allowed_count -= 1
1068 raise MergeFailure('Merge was not successful due to mergeability'
1069 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001070 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001071
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001072 def setCommitStatus(self, project, sha, state, url='', description='',
1073 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001074 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001075 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001076 super(FakeGithubConnection, self).setCommitStatus(
1077 project, sha, state,
1078 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001079
Jan Hruban16ad31f2015-11-07 14:39:07 +01001080 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001081 # record that this got reported
1082 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001083 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001084 pull_request.addLabel(label)
1085
1086 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001087 # record that this got reported
1088 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001089 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001090 pull_request.removeLabel(label)
1091
Gregory Haynes4fc12542015-04-22 20:38:06 -07001092
Clark Boylanb640e052014-04-03 16:41:46 -07001093class BuildHistory(object):
1094 def __init__(self, **kw):
1095 self.__dict__.update(kw)
1096
1097 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001098 return ("<Completed build, result: %s name: %s uuid: %s "
1099 "changes: %s ref: %s>" %
1100 (self.result, self.name, self.uuid,
1101 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001102
1103
Clark Boylanb640e052014-04-03 16:41:46 -07001104class FakeStatsd(threading.Thread):
1105 def __init__(self):
1106 threading.Thread.__init__(self)
1107 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001108 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001109 self.sock.bind(('', 0))
1110 self.port = self.sock.getsockname()[1]
1111 self.wake_read, self.wake_write = os.pipe()
1112 self.stats = []
1113
1114 def run(self):
1115 while True:
1116 poll = select.poll()
1117 poll.register(self.sock, select.POLLIN)
1118 poll.register(self.wake_read, select.POLLIN)
1119 ret = poll.poll()
1120 for (fd, event) in ret:
1121 if fd == self.sock.fileno():
1122 data = self.sock.recvfrom(1024)
1123 if not data:
1124 return
1125 self.stats.append(data[0])
1126 if fd == self.wake_read:
1127 return
1128
1129 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001130 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001131
1132
James E. Blaire1767bc2016-08-02 10:00:27 -07001133class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001134 log = logging.getLogger("zuul.test")
1135
Paul Belanger174a8272017-03-14 13:20:10 -04001136 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001137 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001138 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001139 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001140 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001141 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001142 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001143 # TODOv3(jeblair): self.node is really "the label of the node
1144 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001145 # keep using it like this, or we may end up exposing more of
1146 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001147 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001148 self.node = None
1149 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001150 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001151 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001152 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001153 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001154 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001155 self.wait_condition = threading.Condition()
1156 self.waiting = False
1157 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001158 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001159 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001160 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001161 items = self.parameters['zuul']['items']
1162 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1163 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001164
James E. Blair3158e282016-08-19 09:34:11 -07001165 def __repr__(self):
1166 waiting = ''
1167 if self.waiting:
1168 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001169 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1170 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001171
Clark Boylanb640e052014-04-03 16:41:46 -07001172 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001173 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001174 self.wait_condition.acquire()
1175 self.wait_condition.notify()
1176 self.waiting = False
1177 self.log.debug("Build %s released" % self.unique)
1178 self.wait_condition.release()
1179
1180 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001181 """Return whether this build is being held.
1182
1183 :returns: Whether the build is being held.
1184 :rtype: bool
1185 """
1186
Clark Boylanb640e052014-04-03 16:41:46 -07001187 self.wait_condition.acquire()
1188 if self.waiting:
1189 ret = True
1190 else:
1191 ret = False
1192 self.wait_condition.release()
1193 return ret
1194
1195 def _wait(self):
1196 self.wait_condition.acquire()
1197 self.waiting = True
1198 self.log.debug("Build %s waiting" % self.unique)
1199 self.wait_condition.wait()
1200 self.wait_condition.release()
1201
1202 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001203 self.log.debug('Running build %s' % self.unique)
1204
Paul Belanger174a8272017-03-14 13:20:10 -04001205 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001206 self.log.debug('Holding build %s' % self.unique)
1207 self._wait()
1208 self.log.debug("Build %s continuing" % self.unique)
1209
James E. Blair412fba82017-01-26 15:00:50 -08001210 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001211 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001212 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001213 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001214 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001215 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001216 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001217
James E. Blaire1767bc2016-08-02 10:00:27 -07001218 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001219
James E. Blaira5dba232016-08-08 15:53:24 -07001220 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001221 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001222 for change in changes:
1223 if self.hasChanges(change):
1224 return True
1225 return False
1226
James E. Blaire7b99a02016-08-05 14:27:34 -07001227 def hasChanges(self, *changes):
1228 """Return whether this build has certain changes in its git repos.
1229
1230 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001231 are expected to be present (in order) in the git repository of
1232 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001233
1234 :returns: Whether the build has the indicated changes.
1235 :rtype: bool
1236
1237 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001238 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001239 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001240 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001241 try:
1242 repo = git.Repo(path)
1243 except NoSuchPathError as e:
1244 self.log.debug('%s' % e)
1245 return False
James E. Blair247cab72017-07-20 16:52:36 -07001246 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001247 commit_message = '%s-1' % change.subject
1248 self.log.debug("Checking if build %s has changes; commit_message "
1249 "%s; repo_messages %s" % (self, commit_message,
1250 repo_messages))
1251 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001252 self.log.debug(" messages do not match")
1253 return False
1254 self.log.debug(" OK")
1255 return True
1256
James E. Blaird8af5422017-05-24 13:59:40 -07001257 def getWorkspaceRepos(self, projects):
1258 """Return workspace git repo objects for the listed projects
1259
1260 :arg list projects: A list of strings, each the canonical name
1261 of a project.
1262
1263 :returns: A dictionary of {name: repo} for every listed
1264 project.
1265 :rtype: dict
1266
1267 """
1268
1269 repos = {}
1270 for project in projects:
1271 path = os.path.join(self.jobdir.src_root, project)
1272 repo = git.Repo(path)
1273 repos[project] = repo
1274 return repos
1275
Clark Boylanb640e052014-04-03 16:41:46 -07001276
James E. Blair107bb252017-10-13 15:53:16 -07001277class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1278 def doMergeChanges(self, merger, items, repo_state):
1279 # Get a merger in order to update the repos involved in this job.
1280 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1281 merger, items, repo_state)
1282 if not commit: # merge conflict
1283 self.recordResult('MERGER_FAILURE')
1284 return commit
1285
1286 def recordResult(self, result):
1287 build = self.executor_server.job_builds[self.job.unique]
1288 self.executor_server.lock.acquire()
1289 self.executor_server.build_history.append(
1290 BuildHistory(name=build.name, result=result, changes=build.changes,
1291 node=build.node, uuid=build.unique,
1292 ref=build.parameters['zuul']['ref'],
1293 parameters=build.parameters, jobdir=build.jobdir,
1294 pipeline=build.parameters['zuul']['pipeline'])
1295 )
1296 self.executor_server.running_builds.remove(build)
1297 del self.executor_server.job_builds[self.job.unique]
1298 self.executor_server.lock.release()
1299
1300 def runPlaybooks(self, args):
1301 build = self.executor_server.job_builds[self.job.unique]
1302 build.jobdir = self.jobdir
1303
1304 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1305 self.recordResult(result)
1306 return result
1307
James E. Blaira86aaf12017-10-15 20:59:50 -07001308 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001309 build = self.executor_server.job_builds[self.job.unique]
1310
1311 if self.executor_server._run_ansible:
1312 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001313 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001314 else:
1315 if playbook.path:
1316 result = build.run()
1317 else:
1318 result = (self.RESULT_NORMAL, 0)
1319 return result
1320
1321 def getHostList(self, args):
1322 self.log.debug("hostlist")
1323 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1324 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001325 if not host['host_vars'].get('ansible_connection'):
1326 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001327
1328 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001329 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001330 host_vars=dict(ansible_connection='local'),
1331 host_keys=[]))
1332 return hosts
1333
1334
Paul Belanger174a8272017-03-14 13:20:10 -04001335class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1336 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001337
Paul Belanger174a8272017-03-14 13:20:10 -04001338 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001339 they will report that they have started but then pause until
1340 released before reporting completion. This attribute may be
1341 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001342 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001343 be explicitly released.
1344
1345 """
James E. Blairfaf81982017-10-10 15:42:26 -07001346
1347 _job_class = RecordingAnsibleJob
1348
James E. Blairf5dbd002015-12-23 15:26:17 -08001349 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001350 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001351 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001352 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001353 self.hold_jobs_in_build = False
1354 self.lock = threading.Lock()
1355 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001356 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001357 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001358 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001359
James E. Blaira5dba232016-08-08 15:53:24 -07001360 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001361 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001362
1363 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001364 :arg Change change: The :py:class:`~tests.base.FakeChange`
1365 instance which should cause the job to fail. This job
1366 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001367
1368 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001369 l = self.fail_tests.get(name, [])
1370 l.append(change)
1371 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001372
James E. Blair962220f2016-08-03 11:22:38 -07001373 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001374 """Release a held build.
1375
1376 :arg str regex: A regular expression which, if supplied, will
1377 cause only builds with matching names to be released. If
1378 not supplied, all builds will be released.
1379
1380 """
James E. Blair962220f2016-08-03 11:22:38 -07001381 builds = self.running_builds[:]
1382 self.log.debug("Releasing build %s (%s)" % (regex,
1383 len(self.running_builds)))
1384 for build in builds:
1385 if not regex or re.match(regex, build.name):
1386 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001387 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001388 build.release()
1389 else:
1390 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001391 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001392 self.log.debug("Done releasing builds %s (%s)" %
1393 (regex, len(self.running_builds)))
1394
Paul Belanger174a8272017-03-14 13:20:10 -04001395 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001396 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001398 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001399 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001400 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001401 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001402 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001403 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001404
1405 def stopJob(self, job):
1406 self.log.debug("handle stop")
1407 parameters = json.loads(job.arguments)
1408 uuid = parameters['uuid']
1409 for build in self.running_builds:
1410 if build.unique == uuid:
1411 build.aborted = True
1412 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001413 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001414
James E. Blaira002b032017-04-18 10:35:48 -07001415 def stop(self):
1416 for build in self.running_builds:
1417 build.release()
1418 super(RecordingExecutorServer, self).stop()
1419
Joshua Hesketh50c21782016-10-13 21:34:14 +11001420
Clark Boylanb640e052014-04-03 16:41:46 -07001421class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001422 """A Gearman server for use in tests.
1423
1424 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1425 added to the queue but will not be distributed to workers
1426 until released. This attribute may be changed at any time and
1427 will take effect for subsequently enqueued jobs, but
1428 previously held jobs will still need to be explicitly
1429 released.
1430
1431 """
1432
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001433 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001434 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001435 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001436 if use_ssl:
1437 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1438 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1439 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1440 else:
1441 ssl_ca = None
1442 ssl_cert = None
1443 ssl_key = None
1444
1445 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1446 ssl_cert=ssl_cert,
1447 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001448
1449 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001450 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1451 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001452 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001453 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001454 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001455 elif job.name.startswith(b'merger:'):
1456 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001457 else:
1458 job.waiting = False
1459 if job.waiting:
1460 continue
1461 if job.name in connection.functions:
1462 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001463 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001464 connection.related_jobs[job.handle] = job
1465 job.worker_connection = connection
1466 job.running = True
1467 return job
1468 return None
1469
1470 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001471 """Release a held job.
1472
1473 :arg str regex: A regular expression which, if supplied, will
1474 cause only jobs with matching names to be released. If
1475 not supplied, all jobs will be released.
1476 """
Clark Boylanb640e052014-04-03 16:41:46 -07001477 released = False
1478 qlen = (len(self.high_queue) + len(self.normal_queue) +
1479 len(self.low_queue))
1480 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1481 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001482 match = False
1483 if job.name == b'executor:execute':
1484 parameters = json.loads(job.arguments.decode('utf8'))
1485 if not regex or re.match(regex, parameters.get('job')):
1486 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001487 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001488 if not regex:
1489 match = True
1490 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001491 self.log.debug("releasing queued job %s" %
1492 job.unique)
1493 job.waiting = False
1494 released = True
1495 else:
1496 self.log.debug("not releasing queued job %s" %
1497 job.unique)
1498 if released:
1499 self.wakeConnections()
1500 qlen = (len(self.high_queue) + len(self.normal_queue) +
1501 len(self.low_queue))
1502 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1503
1504
1505class FakeSMTP(object):
1506 log = logging.getLogger('zuul.FakeSMTP')
1507
1508 def __init__(self, messages, server, port):
1509 self.server = server
1510 self.port = port
1511 self.messages = messages
1512
1513 def sendmail(self, from_email, to_email, msg):
1514 self.log.info("Sending email from %s, to %s, with msg %s" % (
1515 from_email, to_email, msg))
1516
1517 headers = msg.split('\n\n', 1)[0]
1518 body = msg.split('\n\n', 1)[1]
1519
1520 self.messages.append(dict(
1521 from_email=from_email,
1522 to_email=to_email,
1523 msg=msg,
1524 headers=headers,
1525 body=body,
1526 ))
1527
1528 return True
1529
1530 def quit(self):
1531 return True
1532
1533
James E. Blairdce6cea2016-12-20 16:45:32 -08001534class FakeNodepool(object):
1535 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001536 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001537
1538 log = logging.getLogger("zuul.test.FakeNodepool")
1539
1540 def __init__(self, host, port, chroot):
1541 self.client = kazoo.client.KazooClient(
1542 hosts='%s:%s%s' % (host, port, chroot))
1543 self.client.start()
1544 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001545 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001546 self.thread = threading.Thread(target=self.run)
1547 self.thread.daemon = True
1548 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001549 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001550
1551 def stop(self):
1552 self._running = False
1553 self.thread.join()
1554 self.client.stop()
1555 self.client.close()
1556
1557 def run(self):
1558 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001559 try:
1560 self._run()
1561 except Exception:
1562 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001563 time.sleep(0.1)
1564
1565 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001566 if self.paused:
1567 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001568 for req in self.getNodeRequests():
1569 self.fulfillRequest(req)
1570
1571 def getNodeRequests(self):
1572 try:
1573 reqids = self.client.get_children(self.REQUEST_ROOT)
1574 except kazoo.exceptions.NoNodeError:
1575 return []
1576 reqs = []
1577 for oid in sorted(reqids):
1578 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001579 try:
1580 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001581 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001582 data['_oid'] = oid
1583 reqs.append(data)
1584 except kazoo.exceptions.NoNodeError:
1585 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001586 return reqs
1587
James E. Blaire18d4602017-01-05 11:17:28 -08001588 def getNodes(self):
1589 try:
1590 nodeids = self.client.get_children(self.NODE_ROOT)
1591 except kazoo.exceptions.NoNodeError:
1592 return []
1593 nodes = []
1594 for oid in sorted(nodeids):
1595 path = self.NODE_ROOT + '/' + oid
1596 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001597 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001598 data['_oid'] = oid
1599 try:
1600 lockfiles = self.client.get_children(path + '/lock')
1601 except kazoo.exceptions.NoNodeError:
1602 lockfiles = []
1603 if lockfiles:
1604 data['_lock'] = True
1605 else:
1606 data['_lock'] = False
1607 nodes.append(data)
1608 return nodes
1609
James E. Blaira38c28e2017-01-04 10:33:20 -08001610 def makeNode(self, request_id, node_type):
1611 now = time.time()
1612 path = '/nodepool/nodes/'
1613 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001614 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001615 provider='test-provider',
1616 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001617 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001618 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001619 public_ipv4='127.0.0.1',
1620 private_ipv4=None,
1621 public_ipv6=None,
1622 allocated_to=request_id,
1623 state='ready',
1624 state_time=now,
1625 created_time=now,
1626 updated_time=now,
1627 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001628 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001629 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001630 if 'fakeuser' in node_type:
1631 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001632 if 'windows' in node_type:
1633 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001634 if 'network' in node_type:
1635 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001636
Clint Byrumf322fe22017-05-10 20:53:12 -07001637 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001638 path = self.client.create(path, data,
1639 makepath=True,
1640 sequence=True)
1641 nodeid = path.split("/")[-1]
1642 return nodeid
1643
James E. Blair6ab79e02017-01-06 10:10:17 -08001644 def addFailRequest(self, request):
1645 self.fail_requests.add(request['_oid'])
1646
James E. Blairdce6cea2016-12-20 16:45:32 -08001647 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001648 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001649 return
1650 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001651 oid = request['_oid']
1652 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001653
James E. Blair6ab79e02017-01-06 10:10:17 -08001654 if oid in self.fail_requests:
1655 request['state'] = 'failed'
1656 else:
1657 request['state'] = 'fulfilled'
1658 nodes = []
1659 for node in request['node_types']:
1660 nodeid = self.makeNode(oid, node)
1661 nodes.append(nodeid)
1662 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001663
James E. Blaira38c28e2017-01-04 10:33:20 -08001664 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001665 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001666 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001667 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001668 try:
1669 self.client.set(path, data)
1670 except kazoo.exceptions.NoNodeError:
1671 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001672
1673
James E. Blair498059b2016-12-20 13:50:13 -08001674class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001675 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001676 super(ChrootedKazooFixture, self).__init__()
1677
1678 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1679 if ':' in zk_host:
1680 host, port = zk_host.split(':')
1681 else:
1682 host = zk_host
1683 port = None
1684
1685 self.zookeeper_host = host
1686
1687 if not port:
1688 self.zookeeper_port = 2181
1689 else:
1690 self.zookeeper_port = int(port)
1691
Clark Boylan621ec9a2017-04-07 17:41:33 -07001692 self.test_id = test_id
1693
James E. Blair498059b2016-12-20 13:50:13 -08001694 def _setUp(self):
1695 # Make sure the test chroot paths do not conflict
1696 random_bits = ''.join(random.choice(string.ascii_lowercase +
1697 string.ascii_uppercase)
1698 for x in range(8))
1699
Clark Boylan621ec9a2017-04-07 17:41:33 -07001700 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001701 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1702
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001703 self.addCleanup(self._cleanup)
1704
James E. Blair498059b2016-12-20 13:50:13 -08001705 # Ensure the chroot path exists and clean up any pre-existing znodes.
1706 _tmp_client = kazoo.client.KazooClient(
1707 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1708 _tmp_client.start()
1709
1710 if _tmp_client.exists(self.zookeeper_chroot):
1711 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1712
1713 _tmp_client.ensure_path(self.zookeeper_chroot)
1714 _tmp_client.stop()
1715 _tmp_client.close()
1716
James E. Blair498059b2016-12-20 13:50:13 -08001717 def _cleanup(self):
1718 '''Remove the chroot path.'''
1719 # Need a non-chroot'ed client to remove the chroot path
1720 _tmp_client = kazoo.client.KazooClient(
1721 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1722 _tmp_client.start()
1723 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1724 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001725 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001726
1727
Joshua Heskethd78b4482015-09-14 16:56:34 -06001728class MySQLSchemaFixture(fixtures.Fixture):
1729 def setUp(self):
1730 super(MySQLSchemaFixture, self).setUp()
1731
1732 random_bits = ''.join(random.choice(string.ascii_lowercase +
1733 string.ascii_uppercase)
1734 for x in range(8))
1735 self.name = '%s_%s' % (random_bits, os.getpid())
1736 self.passwd = uuid.uuid4().hex
1737 db = pymysql.connect(host="localhost",
1738 user="openstack_citest",
1739 passwd="openstack_citest",
1740 db="openstack_citest")
1741 cur = db.cursor()
1742 cur.execute("create database %s" % self.name)
1743 cur.execute(
1744 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1745 (self.name, self.name, self.passwd))
1746 cur.execute("flush privileges")
1747
1748 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1749 self.passwd,
1750 self.name)
1751 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1752 self.addCleanup(self.cleanup)
1753
1754 def cleanup(self):
1755 db = pymysql.connect(host="localhost",
1756 user="openstack_citest",
1757 passwd="openstack_citest",
1758 db="openstack_citest")
1759 cur = db.cursor()
1760 cur.execute("drop database %s" % self.name)
1761 cur.execute("drop user '%s'@'localhost'" % self.name)
1762 cur.execute("flush privileges")
1763
1764
Maru Newby3fe5f852015-01-13 04:22:14 +00001765class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001766 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001767 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001768
James E. Blair1c236df2017-02-01 14:07:24 -08001769 def attachLogs(self, *args):
1770 def reader():
1771 self._log_stream.seek(0)
1772 while True:
1773 x = self._log_stream.read(4096)
1774 if not x:
1775 break
1776 yield x.encode('utf8')
1777 content = testtools.content.content_from_reader(
1778 reader,
1779 testtools.content_type.UTF8_TEXT,
1780 False)
1781 self.addDetail('logging', content)
1782
Clark Boylanb640e052014-04-03 16:41:46 -07001783 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001784 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001785 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1786 try:
1787 test_timeout = int(test_timeout)
1788 except ValueError:
1789 # If timeout value is invalid do not set a timeout.
1790 test_timeout = 0
1791 if test_timeout > 0:
1792 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1793
1794 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1795 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1796 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1797 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1798 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1799 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1800 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1801 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1802 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1803 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001804 self._log_stream = StringIO()
1805 self.addOnException(self.attachLogs)
1806 else:
1807 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001808
James E. Blair73b41772017-05-22 13:22:55 -07001809 # NOTE(jeblair): this is temporary extra debugging to try to
1810 # track down a possible leak.
1811 orig_git_repo_init = git.Repo.__init__
1812
1813 def git_repo_init(myself, *args, **kw):
1814 orig_git_repo_init(myself, *args, **kw)
1815 self.log.debug("Created git repo 0x%x %s" %
1816 (id(myself), repr(myself)))
1817
1818 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1819 git_repo_init))
1820
James E. Blair1c236df2017-02-01 14:07:24 -08001821 handler = logging.StreamHandler(self._log_stream)
1822 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1823 '%(levelname)-8s %(message)s')
1824 handler.setFormatter(formatter)
1825
1826 logger = logging.getLogger()
1827 logger.setLevel(logging.DEBUG)
1828 logger.addHandler(handler)
1829
Clark Boylan3410d532017-04-25 12:35:29 -07001830 # Make sure we don't carry old handlers around in process state
1831 # which slows down test runs
1832 self.addCleanup(logger.removeHandler, handler)
1833 self.addCleanup(handler.close)
1834 self.addCleanup(handler.flush)
1835
James E. Blair1c236df2017-02-01 14:07:24 -08001836 # NOTE(notmorgan): Extract logging overrides for specific
1837 # libraries from the OS_LOG_DEFAULTS env and create loggers
1838 # for each. This is used to limit the output during test runs
1839 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001840 log_defaults_from_env = os.environ.get(
1841 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001842 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001843
James E. Blairdce6cea2016-12-20 16:45:32 -08001844 if log_defaults_from_env:
1845 for default in log_defaults_from_env.split(','):
1846 try:
1847 name, level_str = default.split('=', 1)
1848 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001849 logger = logging.getLogger(name)
1850 logger.setLevel(level)
1851 logger.addHandler(handler)
1852 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001853 except ValueError:
1854 # NOTE(notmorgan): Invalid format of the log default,
1855 # skip and don't try and apply a logger for the
1856 # specified module
1857 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001858
Maru Newby3fe5f852015-01-13 04:22:14 +00001859
1860class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001861 """A test case with a functioning Zuul.
1862
1863 The following class variables are used during test setup and can
1864 be overidden by subclasses but are effectively read-only once a
1865 test method starts running:
1866
1867 :cvar str config_file: This points to the main zuul config file
1868 within the fixtures directory. Subclasses may override this
1869 to obtain a different behavior.
1870
1871 :cvar str tenant_config_file: This is the tenant config file
1872 (which specifies from what git repos the configuration should
1873 be loaded). It defaults to the value specified in
1874 `config_file` but can be overidden by subclasses to obtain a
1875 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001876 configuration. See also the :py:func:`simple_layout`
1877 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001878
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001879 :cvar bool create_project_keys: Indicates whether Zuul should
1880 auto-generate keys for each project, or whether the test
1881 infrastructure should insert dummy keys to save time during
1882 startup. Defaults to False.
1883
James E. Blaire7b99a02016-08-05 14:27:34 -07001884 The following are instance variables that are useful within test
1885 methods:
1886
1887 :ivar FakeGerritConnection fake_<connection>:
1888 A :py:class:`~tests.base.FakeGerritConnection` will be
1889 instantiated for each connection present in the config file
1890 and stored here. For instance, `fake_gerrit` will hold the
1891 FakeGerritConnection object for a connection named `gerrit`.
1892
1893 :ivar FakeGearmanServer gearman_server: An instance of
1894 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1895 server that all of the Zuul components in this test use to
1896 communicate with each other.
1897
Paul Belanger174a8272017-03-14 13:20:10 -04001898 :ivar RecordingExecutorServer executor_server: An instance of
1899 :py:class:`~tests.base.RecordingExecutorServer` which is the
1900 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001901
1902 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1903 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001904 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001905 list upon completion.
1906
1907 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1908 objects representing completed builds. They are appended to
1909 the list in the order they complete.
1910
1911 """
1912
James E. Blair83005782015-12-11 14:46:03 -08001913 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001914 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001915 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001916 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001917
1918 def _startMerger(self):
1919 self.merge_server = zuul.merger.server.MergeServer(self.config,
1920 self.connections)
1921 self.merge_server.start()
1922
Maru Newby3fe5f852015-01-13 04:22:14 +00001923 def setUp(self):
1924 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001925
1926 self.setupZK()
1927
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001928 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001929 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001930 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1931 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001932 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001933 tmp_root = tempfile.mkdtemp(
1934 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001935 self.test_root = os.path.join(tmp_root, "zuul-test")
1936 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001937 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001938 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001939 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001940 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1941 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001942
1943 if os.path.exists(self.test_root):
1944 shutil.rmtree(self.test_root)
1945 os.makedirs(self.test_root)
1946 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001947 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001948 os.makedirs(self.merger_state_root)
1949 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001950
1951 # Make per test copy of Configuration.
1952 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001953 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1954 if not os.path.exists(self.private_key_file):
1955 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1956 shutil.copy(src_private_key_file, self.private_key_file)
1957 shutil.copy('{}.pub'.format(src_private_key_file),
1958 '{}.pub'.format(self.private_key_file))
1959 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001960 self.config.set('scheduler', 'tenant_config',
1961 os.path.join(
1962 FIXTURE_DIR,
1963 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001964 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001965 self.config.set(
1966 'scheduler', 'command_socket',
1967 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001968 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001969 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001970 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001971 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001972 self.config.set(
1973 'executor', 'command_socket',
1974 os.path.join(self.test_root, 'executor.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001975
Clark Boylanb640e052014-04-03 16:41:46 -07001976 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001977 if self.config.has_section('statsd'):
1978 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001979 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001980
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001981 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001982
1983 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001984 self.log.info("Gearman server on port %s" %
1985 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001986 if self.use_ssl:
1987 self.log.info('SSL enabled for gearman')
1988 self.config.set(
1989 'gearman', 'ssl_ca',
1990 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1991 self.config.set(
1992 'gearman', 'ssl_cert',
1993 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1994 self.config.set(
1995 'gearman', 'ssl_key',
1996 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001997
Jesse Keating80730e62017-09-14 15:35:11 -06001998 self.rpcclient = zuul.rpcclient.RPCClient(
1999 self.config.get('gearman', 'server'),
2000 self.gearman_server.port,
2001 get_default(self.config, 'gearman', 'ssl_key'),
2002 get_default(self.config, 'gearman', 'ssl_cert'),
2003 get_default(self.config, 'gearman', 'ssl_ca'))
2004
James E. Blaire511d2f2016-12-08 15:22:26 -08002005 gerritsource.GerritSource.replication_timeout = 1.5
2006 gerritsource.GerritSource.replication_retry_interval = 0.5
2007 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002008
Joshua Hesketh352264b2015-08-11 23:42:08 +10002009 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002010 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002011
Jan Hruban7083edd2015-08-21 14:00:54 +02002012 self.webapp = zuul.webapp.WebApp(
2013 self.sched, port=0, listen_address='127.0.0.1')
2014
Jan Hruban6b71aff2015-10-22 16:58:08 +02002015 self.event_queues = [
2016 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002017 self.sched.trigger_event_queue,
2018 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002019 ]
2020
James E. Blairfef78942016-03-11 16:28:56 -08002021 self.configure_connections()
Jesse Keating80730e62017-09-14 15:35:11 -06002022 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002023
Paul Belanger174a8272017-03-14 13:20:10 -04002024 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002025 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002026 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002027 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002028 _test_root=self.test_root,
2029 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002030 self.executor_server.start()
2031 self.history = self.executor_server.build_history
2032 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002033
Paul Belanger174a8272017-03-14 13:20:10 -04002034 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002035 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002036 self.merge_client = zuul.merger.client.MergeClient(
2037 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002038 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002039 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002040 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002041
James E. Blair0d5a36e2017-02-21 10:53:44 -05002042 self.fake_nodepool = FakeNodepool(
2043 self.zk_chroot_fixture.zookeeper_host,
2044 self.zk_chroot_fixture.zookeeper_port,
2045 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002046
Paul Belanger174a8272017-03-14 13:20:10 -04002047 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002048 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002049 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002050 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002051
Clark Boylanb640e052014-04-03 16:41:46 -07002052 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002053 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002054 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002055 # Cleanups are run in reverse order
2056 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002057 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002058 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002059
James E. Blairb9c0d772017-03-03 14:34:49 -08002060 self.sched.reconfigure(self.config)
2061 self.sched.resume()
2062
Tobias Henkel7df274b2017-05-26 17:41:11 +02002063 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002064 # Set up gerrit related fakes
2065 # Set a changes database so multiple FakeGerrit's can report back to
2066 # a virtual canonical database given by the configured hostname
2067 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002068 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002069
2070 def getGerritConnection(driver, name, config):
2071 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2072 con = FakeGerritConnection(driver, name, config,
2073 changes_db=db,
2074 upstream_root=self.upstream_root)
2075 self.event_queues.append(con.event_queue)
2076 setattr(self, 'fake_' + name, con)
2077 return con
2078
2079 self.useFixture(fixtures.MonkeyPatch(
2080 'zuul.driver.gerrit.GerritDriver.getConnection',
2081 getGerritConnection))
2082
Gregory Haynes4fc12542015-04-22 20:38:06 -07002083 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002084 server = config.get('server', 'github.com')
2085 db = self.github_changes_dbs.setdefault(server, {})
Gregory Haynes4fc12542015-04-22 20:38:06 -07002086 con = FakeGithubConnection(driver, name, config,
Jesse Keating80730e62017-09-14 15:35:11 -06002087 self.rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -08002088 changes_db=db,
Gregory Haynes4fc12542015-04-22 20:38:06 -07002089 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002090 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002091 setattr(self, 'fake_' + name, con)
2092 return con
2093
2094 self.useFixture(fixtures.MonkeyPatch(
2095 'zuul.driver.github.GithubDriver.getConnection',
2096 getGithubConnection))
2097
James E. Blaire511d2f2016-12-08 15:22:26 -08002098 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002099 # TODO(jhesketh): This should come from lib.connections for better
2100 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002101 # Register connections from the config
2102 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002103
Joshua Hesketh352264b2015-08-11 23:42:08 +10002104 def FakeSMTPFactory(*args, **kw):
2105 args = [self.smtp_messages] + list(args)
2106 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002107
Joshua Hesketh352264b2015-08-11 23:42:08 +10002108 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002109
James E. Blaire511d2f2016-12-08 15:22:26 -08002110 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002111 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002112 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002113
James E. Blair83005782015-12-11 14:46:03 -08002114 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002115 # This creates the per-test configuration object. It can be
2116 # overriden by subclasses, but should not need to be since it
2117 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002118 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002119 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002120
James E. Blair39840362017-06-23 20:34:02 +01002121 sections = ['zuul', 'scheduler', 'executor', 'merger']
2122 for section in sections:
2123 if not self.config.has_section(section):
2124 self.config.add_section(section)
2125
James E. Blair06cc3922017-04-19 10:08:10 -07002126 if not self.setupSimpleLayout():
2127 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002128 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002129 self.tenant_config_file)
2130 git_path = os.path.join(
2131 os.path.dirname(
2132 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2133 'git')
2134 if os.path.exists(git_path):
2135 for reponame in os.listdir(git_path):
2136 project = reponame.replace('_', '/')
2137 self.copyDirToRepo(project,
2138 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002139 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002140 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002141 self.setupAllProjectKeys()
2142
James E. Blair06cc3922017-04-19 10:08:10 -07002143 def setupSimpleLayout(self):
2144 # If the test method has been decorated with a simple_layout,
2145 # use that instead of the class tenant_config_file. Set up a
2146 # single config-project with the specified layout, and
2147 # initialize repos for all of the 'project' entries which
2148 # appear in the layout.
2149 test_name = self.id().split('.')[-1]
2150 test = getattr(self, test_name)
2151 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002152 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002153 else:
2154 return False
2155
James E. Blairb70e55a2017-04-19 12:57:02 -07002156 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002157 path = os.path.join(FIXTURE_DIR, path)
2158 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002159 data = f.read()
2160 layout = yaml.safe_load(data)
2161 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002162 untrusted_projects = []
2163 for item in layout:
2164 if 'project' in item:
2165 name = item['project']['name']
2166 untrusted_projects.append(name)
2167 self.init_repo(name)
2168 self.addCommitToRepo(name, 'initial commit',
2169 files={'README': ''},
2170 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002171 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002172 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002173 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002174 for fn in zuul.configloader.as_list(
2175 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002176 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002177 for fn in zuul.configloader.as_list(
2178 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002179 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002180
2181 root = os.path.join(self.test_root, "config")
2182 if not os.path.exists(root):
2183 os.makedirs(root)
2184 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2185 config = [{'tenant':
2186 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002187 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002188 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002189 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002190 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002191 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002192 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002193 os.path.join(FIXTURE_DIR, f.name))
2194
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002195 self.init_repo('org/common-config')
2196 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002197 files, branch='master', tag='init')
2198
2199 return True
2200
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002201 def setupAllProjectKeys(self):
2202 if self.create_project_keys:
2203 return
2204
James E. Blair39840362017-06-23 20:34:02 +01002205 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002206 with open(os.path.join(FIXTURE_DIR, path)) as f:
2207 tenant_config = yaml.safe_load(f.read())
2208 for tenant in tenant_config:
2209 sources = tenant['tenant']['source']
2210 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002211 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002212 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002213 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002214 self.setupProjectKeys(source, project)
2215
2216 def setupProjectKeys(self, source, project):
2217 # Make sure we set up an RSA key for the project so that we
2218 # don't spend time generating one:
2219
James E. Blair6459db12017-06-29 14:57:20 -07002220 if isinstance(project, dict):
2221 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002222 key_root = os.path.join(self.state_root, 'keys')
2223 if not os.path.isdir(key_root):
2224 os.mkdir(key_root, 0o700)
2225 private_key_file = os.path.join(key_root, source, project + '.pem')
2226 private_key_dir = os.path.dirname(private_key_file)
2227 self.log.debug("Installing test keys for project %s at %s" % (
2228 project, private_key_file))
2229 if not os.path.isdir(private_key_dir):
2230 os.makedirs(private_key_dir)
2231 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2232 with open(private_key_file, 'w') as o:
2233 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002234
James E. Blair498059b2016-12-20 13:50:13 -08002235 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002236 self.zk_chroot_fixture = self.useFixture(
2237 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002238 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002239 self.zk_chroot_fixture.zookeeper_host,
2240 self.zk_chroot_fixture.zookeeper_port,
2241 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002242
James E. Blair96c6bf82016-01-15 16:20:40 -08002243 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002244 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002245
2246 files = {}
2247 for (dirpath, dirnames, filenames) in os.walk(source_path):
2248 for filename in filenames:
2249 test_tree_filepath = os.path.join(dirpath, filename)
2250 common_path = os.path.commonprefix([test_tree_filepath,
2251 source_path])
2252 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2253 with open(test_tree_filepath, 'r') as f:
2254 content = f.read()
2255 files[relative_filepath] = content
2256 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002257 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002258
James E. Blaire18d4602017-01-05 11:17:28 -08002259 def assertNodepoolState(self):
2260 # Make sure that there are no pending requests
2261
2262 requests = self.fake_nodepool.getNodeRequests()
2263 self.assertEqual(len(requests), 0)
2264
2265 nodes = self.fake_nodepool.getNodes()
2266 for node in nodes:
2267 self.assertFalse(node['_lock'], "Node %s is locked" %
2268 (node['_oid'],))
2269
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002270 def assertNoGeneratedKeys(self):
2271 # Make sure that Zuul did not generate any project keys
2272 # (unless it was supposed to).
2273
2274 if self.create_project_keys:
2275 return
2276
2277 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2278 test_key = i.read()
2279
2280 key_root = os.path.join(self.state_root, 'keys')
2281 for root, dirname, files in os.walk(key_root):
2282 for fn in files:
2283 with open(os.path.join(root, fn)) as f:
2284 self.assertEqual(test_key, f.read())
2285
Clark Boylanb640e052014-04-03 16:41:46 -07002286 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002287 self.log.debug("Assert final state")
2288 # Make sure no jobs are running
2289 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002290 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002291 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002292 gc.collect()
2293 for obj in gc.get_objects():
2294 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002295 self.log.debug("Leaked git repo object: 0x%x %s" %
2296 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002297 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002298 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002299 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002300 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002301 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002302 for tenant in self.sched.abide.tenants.values():
2303 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002304 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002305 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002306
2307 def shutdown(self):
2308 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002309 self.executor_server.hold_jobs_in_build = False
2310 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002311 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002312 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002313 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002314 self.sched.stop()
2315 self.sched.join()
2316 self.statsd.stop()
2317 self.statsd.join()
2318 self.webapp.stop()
2319 self.webapp.join()
Jesse Keating80730e62017-09-14 15:35:11 -06002320 self.rpcclient.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07002321 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002322 self.fake_nodepool.stop()
2323 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002324 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002325 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002326 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002327 # Further the pydevd threads also need to be whitelisted so debugging
2328 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002329 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002330 'pydevd.CommandThread',
2331 'pydevd.Reader',
2332 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002333 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002334 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002335 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002336 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002337 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002338 log_str = ""
2339 for thread_id, stack_frame in sys._current_frames().items():
2340 log_str += "Thread: %s\n" % thread_id
2341 log_str += "".join(traceback.format_stack(stack_frame))
2342 self.log.debug(log_str)
2343 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002344
James E. Blaira002b032017-04-18 10:35:48 -07002345 def assertCleanShutdown(self):
2346 pass
2347
James E. Blairc4ba97a2017-04-19 16:26:24 -07002348 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002349 parts = project.split('/')
2350 path = os.path.join(self.upstream_root, *parts[:-1])
2351 if not os.path.exists(path):
2352 os.makedirs(path)
2353 path = os.path.join(self.upstream_root, project)
2354 repo = git.Repo.init(path)
2355
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002356 with repo.config_writer() as config_writer:
2357 config_writer.set_value('user', 'email', 'user@example.com')
2358 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002359
Clark Boylanb640e052014-04-03 16:41:46 -07002360 repo.index.commit('initial commit')
2361 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002362 if tag:
2363 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002364
James E. Blair97d902e2014-08-21 13:25:56 -07002365 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002366 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002367 repo.git.clean('-x', '-f', '-d')
2368
James E. Blair97d902e2014-08-21 13:25:56 -07002369 def create_branch(self, project, branch):
2370 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002371 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002372 fn = os.path.join(path, 'README')
2373
2374 branch_head = repo.create_head(branch)
2375 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002376 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002377 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002378 f.close()
2379 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002380 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002381
James E. Blair97d902e2014-08-21 13:25:56 -07002382 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002383 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002384 repo.git.clean('-x', '-f', '-d')
2385
Sachi King9f16d522016-03-16 12:20:45 +11002386 def create_commit(self, project):
2387 path = os.path.join(self.upstream_root, project)
2388 repo = git.Repo(path)
2389 repo.head.reference = repo.heads['master']
2390 file_name = os.path.join(path, 'README')
2391 with open(file_name, 'a') as f:
2392 f.write('creating fake commit\n')
2393 repo.index.add([file_name])
2394 commit = repo.index.commit('Creating a fake commit')
2395 return commit.hexsha
2396
James E. Blairf4a5f022017-04-18 14:01:10 -07002397 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002398 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002399 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002400 while len(self.builds):
2401 self.release(self.builds[0])
2402 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002403 i += 1
2404 if count is not None and i >= count:
2405 break
James E. Blairb8c16472015-05-05 14:55:26 -07002406
James E. Blairdf25ddc2017-07-08 07:57:09 -07002407 def getSortedBuilds(self):
2408 "Return the list of currently running builds sorted by name"
2409
2410 return sorted(self.builds, key=lambda x: x.name)
2411
Clark Boylanb640e052014-04-03 16:41:46 -07002412 def release(self, job):
2413 if isinstance(job, FakeBuild):
2414 job.release()
2415 else:
2416 job.waiting = False
2417 self.log.debug("Queued job %s released" % job.unique)
2418 self.gearman_server.wakeConnections()
2419
2420 def getParameter(self, job, name):
2421 if isinstance(job, FakeBuild):
2422 return job.parameters[name]
2423 else:
2424 parameters = json.loads(job.arguments)
2425 return parameters[name]
2426
Clark Boylanb640e052014-04-03 16:41:46 -07002427 def haveAllBuildsReported(self):
2428 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002429 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002430 return False
2431 # Find out if every build that the worker has completed has been
2432 # reported back to Zuul. If it hasn't then that means a Gearman
2433 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002434 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002435 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002436 if not zbuild:
2437 # It has already been reported
2438 continue
2439 # It hasn't been reported yet.
2440 return False
2441 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002442 worker = self.executor_server.executor_worker
2443 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002444 if connection.state == 'GRAB_WAIT':
2445 return False
2446 return True
2447
2448 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002449 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002450 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002451 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002452 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002453 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002454 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002455 for j in conn.related_jobs.values():
2456 if j.unique == build.uuid:
2457 client_job = j
2458 break
2459 if not client_job:
2460 self.log.debug("%s is not known to the gearman client" %
2461 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002462 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002463 if not client_job.handle:
2464 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002465 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002466 server_job = self.gearman_server.jobs.get(client_job.handle)
2467 if not server_job:
2468 self.log.debug("%s is not known to the gearman server" %
2469 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002470 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002471 if not hasattr(server_job, 'waiting'):
2472 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002473 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002474 if server_job.waiting:
2475 continue
James E. Blair17302972016-08-10 16:11:42 -07002476 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002477 self.log.debug("%s has not reported start" % build)
2478 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002479 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002480 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002481 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002482 if worker_build:
2483 if worker_build.isWaiting():
2484 continue
2485 else:
2486 self.log.debug("%s is running" % worker_build)
2487 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002488 else:
James E. Blair962220f2016-08-03 11:22:38 -07002489 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002490 return False
James E. Blaira002b032017-04-18 10:35:48 -07002491 for (build_uuid, job_worker) in \
2492 self.executor_server.job_workers.items():
2493 if build_uuid not in seen_builds:
2494 self.log.debug("%s is not finalized" % build_uuid)
2495 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002496 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002497
James E. Blairdce6cea2016-12-20 16:45:32 -08002498 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002499 if self.fake_nodepool.paused:
2500 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002501 if self.sched.nodepool.requests:
2502 return False
2503 return True
2504
James E. Blaira615c362017-10-02 17:34:42 -07002505 def areAllMergeJobsWaiting(self):
2506 for client_job in list(self.merge_client.jobs):
2507 if not client_job.handle:
2508 self.log.debug("%s has no handle" % client_job)
2509 return False
2510 server_job = self.gearman_server.jobs.get(client_job.handle)
2511 if not server_job:
2512 self.log.debug("%s is not known to the gearman server" %
2513 client_job)
2514 return False
2515 if not hasattr(server_job, 'waiting'):
2516 self.log.debug("%s is being enqueued" % server_job)
2517 return False
2518 if server_job.waiting:
2519 self.log.debug("%s is waiting" % server_job)
2520 continue
2521 self.log.debug("%s is not waiting" % server_job)
2522 return False
2523 return True
2524
Jan Hruban6b71aff2015-10-22 16:58:08 +02002525 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002526 for event_queue in self.event_queues:
2527 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002528
2529 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002530 for event_queue in self.event_queues:
2531 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002532
Clark Boylanb640e052014-04-03 16:41:46 -07002533 def waitUntilSettled(self):
2534 self.log.debug("Waiting until settled...")
2535 start = time.time()
2536 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002537 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002538 self.log.error("Timeout waiting for Zuul to settle")
2539 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002540 for event_queue in self.event_queues:
2541 self.log.error(" %s: %s" %
2542 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002543 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002544 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002545 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002546 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002547 self.log.error("All requests completed: %s" %
2548 (self.areAllNodeRequestsComplete(),))
2549 self.log.error("Merge client jobs: %s" %
2550 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002551 raise Exception("Timeout waiting for Zuul to settle")
2552 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002553
Paul Belanger174a8272017-03-14 13:20:10 -04002554 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002555 # have all build states propogated to zuul?
2556 if self.haveAllBuildsReported():
2557 # Join ensures that the queue is empty _and_ events have been
2558 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002559 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002560 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002561 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002562 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002563 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002564 self.areAllNodeRequestsComplete() and
2565 all(self.eventQueuesEmpty())):
2566 # The queue empty check is placed at the end to
2567 # ensure that if a component adds an event between
2568 # when locked the run handler and checked that the
2569 # components were stable, we don't erroneously
2570 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002571 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002572 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002573 self.log.debug("...settled.")
2574 return
2575 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002576 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002577 self.sched.wake_event.wait(0.1)
2578
2579 def countJobResults(self, jobs, result):
2580 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002581 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002582
Monty Taylor0d926122017-05-24 08:07:56 -05002583 def getBuildByName(self, name):
2584 for build in self.builds:
2585 if build.name == name:
2586 return build
2587 raise Exception("Unable to find build %s" % name)
2588
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002589 def assertJobNotInHistory(self, name, project=None):
2590 for job in self.history:
2591 if (project is None or
2592 job.parameters['zuul']['project']['name'] == project):
2593 self.assertNotEqual(job.name, name,
2594 'Job %s found in history' % name)
2595
James E. Blair96c6bf82016-01-15 16:20:40 -08002596 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002597 for job in self.history:
2598 if (job.name == name and
2599 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002600 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002601 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002602 raise Exception("Unable to find job %s in history" % name)
2603
2604 def assertEmptyQueues(self):
2605 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002606 for tenant in self.sched.abide.tenants.values():
2607 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002608 for pipeline_queue in pipeline.queues:
2609 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002610 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002611 pipeline.name, pipeline_queue.name,
2612 pipeline_queue.queue))
2613 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002614 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002615
2616 def assertReportedStat(self, key, value=None, kind=None):
2617 start = time.time()
2618 while time.time() < (start + 5):
2619 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002620 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002621 if key == k:
2622 if value is None and kind is None:
2623 return
2624 elif value:
2625 if value == v:
2626 return
2627 elif kind:
2628 if v.endswith('|' + kind):
2629 return
2630 time.sleep(0.1)
2631
Clark Boylanb640e052014-04-03 16:41:46 -07002632 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002633
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002634 def assertBuilds(self, builds):
2635 """Assert that the running builds are as described.
2636
2637 The list of running builds is examined and must match exactly
2638 the list of builds described by the input.
2639
2640 :arg list builds: A list of dictionaries. Each item in the
2641 list must match the corresponding build in the build
2642 history, and each element of the dictionary must match the
2643 corresponding attribute of the build.
2644
2645 """
James E. Blair3158e282016-08-19 09:34:11 -07002646 try:
2647 self.assertEqual(len(self.builds), len(builds))
2648 for i, d in enumerate(builds):
2649 for k, v in d.items():
2650 self.assertEqual(
2651 getattr(self.builds[i], k), v,
2652 "Element %i in builds does not match" % (i,))
2653 except Exception:
2654 for build in self.builds:
2655 self.log.error("Running build: %s" % build)
2656 else:
2657 self.log.error("No running builds")
2658 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002659
James E. Blairb536ecc2016-08-31 10:11:42 -07002660 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002661 """Assert that the completed builds are as described.
2662
2663 The list of completed builds is examined and must match
2664 exactly the list of builds described by the input.
2665
2666 :arg list history: A list of dictionaries. Each item in the
2667 list must match the corresponding build in the build
2668 history, and each element of the dictionary must match the
2669 corresponding attribute of the build.
2670
James E. Blairb536ecc2016-08-31 10:11:42 -07002671 :arg bool ordered: If true, the history must match the order
2672 supplied, if false, the builds are permitted to have
2673 arrived in any order.
2674
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002675 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002676 def matches(history_item, item):
2677 for k, v in item.items():
2678 if getattr(history_item, k) != v:
2679 return False
2680 return True
James E. Blair3158e282016-08-19 09:34:11 -07002681 try:
2682 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002683 if ordered:
2684 for i, d in enumerate(history):
2685 if not matches(self.history[i], d):
2686 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002687 "Element %i in history does not match %s" %
2688 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002689 else:
2690 unseen = self.history[:]
2691 for i, d in enumerate(history):
2692 found = False
2693 for unseen_item in unseen:
2694 if matches(unseen_item, d):
2695 found = True
2696 unseen.remove(unseen_item)
2697 break
2698 if not found:
2699 raise Exception("No match found for element %i "
2700 "in history" % (i,))
2701 if unseen:
2702 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002703 except Exception:
2704 for build in self.history:
2705 self.log.error("Completed build: %s" % build)
2706 else:
2707 self.log.error("No completed builds")
2708 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002709
James E. Blair6ac368c2016-12-22 18:07:20 -08002710 def printHistory(self):
2711 """Log the build history.
2712
2713 This can be useful during tests to summarize what jobs have
2714 completed.
2715
2716 """
2717 self.log.debug("Build history:")
2718 for build in self.history:
2719 self.log.debug(build)
2720
James E. Blair59fdbac2015-12-07 17:08:06 -08002721 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002722 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2723
James E. Blair9ea70072017-04-19 16:05:30 -07002724 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002725 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002726 if not os.path.exists(root):
2727 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002728 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2729 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002730- tenant:
2731 name: openstack
2732 source:
2733 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002734 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002735 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002736 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002737 - org/project
2738 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002739 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002740 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002741 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002742 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002743 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002744
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002745 def addTagToRepo(self, project, name, sha):
2746 path = os.path.join(self.upstream_root, project)
2747 repo = git.Repo(path)
2748 repo.git.tag(name, sha)
2749
2750 def delTagFromRepo(self, project, name):
2751 path = os.path.join(self.upstream_root, project)
2752 repo = git.Repo(path)
2753 repo.git.tag('-d', name)
2754
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002755 def addCommitToRepo(self, project, message, files,
2756 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002757 path = os.path.join(self.upstream_root, project)
2758 repo = git.Repo(path)
2759 repo.head.reference = branch
2760 zuul.merger.merger.reset_repo_to_head(repo)
2761 for fn, content in files.items():
2762 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002763 try:
2764 os.makedirs(os.path.dirname(fn))
2765 except OSError:
2766 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002767 with open(fn, 'w') as f:
2768 f.write(content)
2769 repo.index.add([fn])
2770 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002771 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002772 repo.heads[branch].commit = commit
2773 repo.head.reference = branch
2774 repo.git.clean('-x', '-f', '-d')
2775 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002776 if tag:
2777 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002778 return before
2779
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002780 def commitConfigUpdate(self, project_name, source_name):
2781 """Commit an update to zuul.yaml
2782
2783 This overwrites the zuul.yaml in the specificed project with
2784 the contents specified.
2785
2786 :arg str project_name: The name of the project containing
2787 zuul.yaml (e.g., common-config)
2788
2789 :arg str source_name: The path to the file (underneath the
2790 test fixture directory) whose contents should be used to
2791 replace zuul.yaml.
2792 """
2793
2794 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002795 files = {}
2796 with open(source_path, 'r') as f:
2797 data = f.read()
2798 layout = yaml.safe_load(data)
2799 files['zuul.yaml'] = data
2800 for item in layout:
2801 if 'job' in item:
2802 jobname = item['job']['name']
2803 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002804 before = self.addCommitToRepo(
2805 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002806 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002807 return before
2808
Clint Byrum627ba362017-08-14 13:20:40 -07002809 def newTenantConfig(self, source_name):
2810 """ Use this to update the tenant config file in tests
2811
2812 This will update self.tenant_config_file to point to a temporary file
2813 for the duration of this particular test. The content of that file will
2814 be taken from FIXTURE_DIR/source_name
2815
2816 After the test the original value of self.tenant_config_file will be
2817 restored.
2818
2819 :arg str source_name: The path of the file under
2820 FIXTURE_DIR that will be used to populate the new tenant
2821 config file.
2822 """
2823 source_path = os.path.join(FIXTURE_DIR, source_name)
2824 orig_tenant_config_file = self.tenant_config_file
2825 with tempfile.NamedTemporaryFile(
2826 delete=False, mode='wb') as new_tenant_config:
2827 self.tenant_config_file = new_tenant_config.name
2828 with open(source_path, mode='rb') as source_tenant_config:
2829 new_tenant_config.write(source_tenant_config.read())
2830 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2831 self.setupAllProjectKeys()
2832 self.log.debug(
2833 'tenant_config_file = {}'.format(self.tenant_config_file))
2834
2835 def _restoreTenantConfig():
2836 self.log.debug(
2837 'restoring tenant_config_file = {}'.format(
2838 orig_tenant_config_file))
2839 os.unlink(self.tenant_config_file)
2840 self.tenant_config_file = orig_tenant_config_file
2841 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2842 self.addCleanup(_restoreTenantConfig)
2843
James E. Blair7fc8daa2016-08-08 15:37:15 -07002844 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002845
James E. Blair7fc8daa2016-08-08 15:37:15 -07002846 """Inject a Fake (Gerrit) event.
2847
2848 This method accepts a JSON-encoded event and simulates Zuul
2849 having received it from Gerrit. It could (and should)
2850 eventually apply to any connection type, but is currently only
2851 used with Gerrit connections. The name of the connection is
2852 used to look up the corresponding server, and the event is
2853 simulated as having been received by all Zuul connections
2854 attached to that server. So if two Gerrit connections in Zuul
2855 are connected to the same Gerrit server, and you invoke this
2856 method specifying the name of one of them, the event will be
2857 received by both.
2858
2859 .. note::
2860
2861 "self.fake_gerrit.addEvent" calls should be migrated to
2862 this method.
2863
2864 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002865 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002866 :arg str event: The JSON-encoded event.
2867
2868 """
2869 specified_conn = self.connections.connections[connection]
2870 for conn in self.connections.connections.values():
2871 if (isinstance(conn, specified_conn.__class__) and
2872 specified_conn.server == conn.server):
2873 conn.addEvent(event)
2874
James E. Blaird8af5422017-05-24 13:59:40 -07002875 def getUpstreamRepos(self, projects):
2876 """Return upstream git repo objects for the listed projects
2877
2878 :arg list projects: A list of strings, each the canonical name
2879 of a project.
2880
2881 :returns: A dictionary of {name: repo} for every listed
2882 project.
2883 :rtype: dict
2884
2885 """
2886
2887 repos = {}
2888 for project in projects:
2889 # FIXME(jeblair): the upstream root does not yet have a
2890 # hostname component; that needs to be added, and this
2891 # line removed:
2892 tmp_project_name = '/'.join(project.split('/')[1:])
2893 path = os.path.join(self.upstream_root, tmp_project_name)
2894 repo = git.Repo(path)
2895 repos[project] = repo
2896 return repos
2897
James E. Blair3f876d52016-07-22 13:07:14 -07002898
2899class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002900 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002901 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002902
Jamie Lennox7655b552017-03-17 12:33:38 +11002903 @contextmanager
2904 def jobLog(self, build):
2905 """Print job logs on assertion errors
2906
2907 This method is a context manager which, if it encounters an
2908 ecxeption, adds the build log to the debug output.
2909
2910 :arg Build build: The build that's being asserted.
2911 """
2912 try:
2913 yield
2914 except Exception:
2915 path = os.path.join(self.test_root, build.uuid,
2916 'work', 'logs', 'job-output.txt')
2917 with open(path) as f:
2918 self.log.debug(f.read())
2919 raise
2920
Joshua Heskethd78b4482015-09-14 16:56:34 -06002921
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002922class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002923 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002924 use_ssl = True
2925
2926
Joshua Heskethd78b4482015-09-14 16:56:34 -06002927class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002928 def setup_config(self):
2929 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002930 for section_name in self.config.sections():
2931 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2932 section_name, re.I)
2933 if not con_match:
2934 continue
2935
2936 if self.config.get(section_name, 'driver') == 'sql':
2937 f = MySQLSchemaFixture()
2938 self.useFixture(f)
2939 if (self.config.get(section_name, 'dburi') ==
2940 '$MYSQL_FIXTURE_DBURI$'):
2941 self.config.set(section_name, 'dburi', f.dburi)