blob: fcc5e841b49f298f0f7a6df7b54b3d8095f95a97 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Jamie Lennox7655b552017-03-17 12:33:38 +110019from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080020import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070021import gc
22import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050023import importlib
24from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070025import json
26import logging
27import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050028import queue
Clark Boylanb640e052014-04-03 16:41:46 -070029import random
30import re
31import select
32import shutil
33import socket
34import string
35import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
Clark Boylan8208c192017-04-24 18:08:08 -070039import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070040import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060041import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050042import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060043
Clark Boylanb640e052014-04-03 16:41:46 -070044
45import git
46import gear
47import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080048import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080049import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060050import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070051import statsd
52import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080053import testtools.content
54import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080055from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000056import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070057
James E. Blaire511d2f2016-12-08 15:22:26 -080058import zuul.driver.gerrit.gerritsource as gerritsource
59import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070060import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.scheduler
62import zuul.webapp
63import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040064import zuul.executor.server
65import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080066import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070067import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070068import zuul.merger.merger
69import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020070import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200137 categories = {'Approved': ('Approved', -1, 1),
138 'Code-Review': ('Code-Review', -2, 2),
139 'Verified': ('Verified', -2, 2)}
140
Clark Boylanb640e052014-04-03 16:41:46 -0700141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700142 status='NEW', upstream_root=None, files={},
143 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700145 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.reported = 0
147 self.queried = 0
148 self.patchsets = []
149 self.number = number
150 self.project = project
151 self.branch = branch
152 self.subject = subject
153 self.latest_patchset = 0
154 self.depends_on_change = None
155 self.needed_by_changes = []
156 self.fail_merge = False
157 self.messages = []
158 self.data = {
159 'branch': branch,
160 'comments': [],
161 'commitMessage': subject,
162 'createdOn': time.time(),
163 'id': 'I' + random_sha1(),
164 'lastUpdated': time.time(),
165 'number': str(number),
166 'open': status == 'NEW',
167 'owner': {'email': 'user@example.com',
168 'name': 'User Name',
169 'username': 'username'},
170 'patchSets': self.patchsets,
171 'project': project,
172 'status': status,
173 'subject': subject,
174 'submitRecords': [],
175 'url': 'https://hostname/%s' % number}
176
177 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700178 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 self.data['submitRecords'] = self.getSubmitRecords()
180 self.open = status == 'NEW'
181
James E. Blair289f5932017-07-27 15:02:29 -0700182 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 path = os.path.join(self.upstream_root, self.project)
184 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700185 if parent is None:
186 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700187 ref = GerritChangeReference.create(
188 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700189 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700191 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700192 repo.git.clean('-x', '-f', '-d')
193
194 path = os.path.join(self.upstream_root, self.project)
195 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700196 for fn, content in files.items():
197 fn = os.path.join(path, fn)
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. Blair72facdc2017-08-17 10:29:12 -0700491 def getFakeBranchCreatedEvent(self, project, branch):
492 path = os.path.join(self.upstream_root, project)
493 repo = git.Repo(path)
494 oldrev = 40 * '0'
495
496 event = {
497 "type": "ref-updated",
498 "submitter": {
499 "name": "User Name",
500 },
501 "refUpdate": {
502 "oldRev": oldrev,
503 "newRev": repo.heads[branch].commit.hexsha,
504 "refName": branch,
505 "project": project,
506 }
507 }
508 return event
509
Clark Boylanb640e052014-04-03 16:41:46 -0700510 def review(self, project, changeid, message, action):
511 number, ps = changeid.split(',')
512 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000513
514 # Add the approval back onto the change (ie simulate what gerrit would
515 # do).
516 # Usually when zuul leaves a review it'll create a feedback loop where
517 # zuul's review enters another gerrit event (which is then picked up by
518 # zuul). However, we can't mimic this behaviour (by adding this
519 # approval event into the queue) as it stops jobs from checking what
520 # happens before this event is triggered. If a job needs to see what
521 # happens they can add their own verified event into the queue.
522 # Nevertheless, we can update change with the new review in gerrit.
523
James E. Blair8b5408c2016-08-08 15:37:46 -0700524 for cat in action.keys():
525 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000526 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000527
Clark Boylanb640e052014-04-03 16:41:46 -0700528 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000529
Clark Boylanb640e052014-04-03 16:41:46 -0700530 if 'submit' in action:
531 change.setMerged()
532 if message:
533 change.setReported()
534
535 def query(self, number):
536 change = self.changes.get(int(number))
537 if change:
538 return change.query()
539 return {}
540
James E. Blairc494d542014-08-06 09:23:52 -0700541 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700542 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700543 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800544 if query.startswith('change:'):
545 # Query a specific changeid
546 changeid = query[len('change:'):]
547 l = [change.query() for change in self.changes.values()
548 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700549 elif query.startswith('message:'):
550 # Query the content of a commit message
551 msg = query[len('message:'):].strip()
552 l = [change.query() for change in self.changes.values()
553 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800554 else:
555 # Query all open changes
556 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700557 return l
James E. Blairc494d542014-08-06 09:23:52 -0700558
Joshua Hesketh352264b2015-08-11 23:42:08 +1000559 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700560 pass
561
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200562 def _uploadPack(self, project):
563 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
564 'multi_ack thin-pack side-band side-band-64k ofs-delta '
565 'shallow no-progress include-tag multi_ack_detailed no-done\n')
566 path = os.path.join(self.upstream_root, project.name)
567 repo = git.Repo(path)
568 for ref in repo.refs:
569 r = ref.object.hexsha + ' ' + ref.path + '\n'
570 ret += '%04x%s' % (len(r) + 4, r)
571 ret += '0000'
572 return ret
573
Joshua Hesketh352264b2015-08-11 23:42:08 +1000574 def getGitUrl(self, project):
575 return os.path.join(self.upstream_root, project.name)
576
Clark Boylanb640e052014-04-03 16:41:46 -0700577
Gregory Haynes4fc12542015-04-22 20:38:06 -0700578class GithubChangeReference(git.Reference):
579 _common_path_default = "refs/pull"
580 _points_to_commits_only = True
581
582
Tobias Henkel64e37a02017-08-02 10:13:30 +0200583class FakeGithub(object):
584
585 class FakeUser(object):
586 def __init__(self, login):
587 self.login = login
588 self.name = "Github User"
589 self.email = "github.user@example.com"
590
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200591 class FakeBranch(object):
592 def __init__(self, branch='master'):
593 self.name = branch
594
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200595 class FakeStatus(object):
596 def __init__(self, state, url, description, context, user):
597 self._state = state
598 self._url = url
599 self._description = description
600 self._context = context
601 self._user = user
602
603 def as_dict(self):
604 return {
605 'state': self._state,
606 'url': self._url,
607 'description': self._description,
608 'context': self._context,
609 'creator': {
610 'login': self._user
611 }
612 }
613
614 class FakeCommit(object):
615 def __init__(self):
616 self._statuses = []
617
618 def set_status(self, state, url, description, context, user):
619 status = FakeGithub.FakeStatus(
620 state, url, description, context, user)
621 # always insert a status to the front of the list, to represent
622 # the last status provided for a commit.
623 self._statuses.insert(0, status)
624
625 def statuses(self):
626 return self._statuses
627
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200628 class FakeRepository(object):
629 def __init__(self):
630 self._branches = [FakeGithub.FakeBranch()]
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200631 self._commits = {}
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200632
Tobias Henkeleca46202017-08-02 20:27:10 +0200633 def branches(self, protected=False):
634 if protected:
635 # simulate there is no protected branch
636 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200637 return self._branches
638
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200639 def create_status(self, sha, state, url, description, context,
640 user='zuul'):
641 # Since we're bypassing github API, which would require a user, we
642 # default the user as 'zuul' here.
643 commit = self._commits.get(sha, None)
644 if commit is None:
645 commit = FakeGithub.FakeCommit()
646 self._commits[sha] = commit
647 commit.set_status(state, url, description, context, user)
648
649 def commit(self, sha):
650 commit = self._commits.get(sha, None)
651 if commit is None:
652 commit = FakeGithub.FakeCommit()
653 self._commits[sha] = commit
654 return commit
655
656 def __init__(self):
657 self._repos = {}
658
Tobias Henkel64e37a02017-08-02 10:13:30 +0200659 def user(self, login):
660 return self.FakeUser(login)
661
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200662 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200663 return self._repos.get((owner, proj), None)
664
665 def repo_from_project(self, project):
666 # This is a convenience method for the tests.
667 owner, proj = project.split('/')
668 return self.repository(owner, proj)
669
670 def addProject(self, project):
671 owner, proj = project.name.split('/')
672 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200673
Tobias Henkel64e37a02017-08-02 10:13:30 +0200674
Gregory Haynes4fc12542015-04-22 20:38:06 -0700675class FakeGithubPullRequest(object):
676
677 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800678 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700679 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700680 """Creates a new PR with several commits.
681 Sends an event about opened PR."""
682 self.github = github
683 self.source = github
684 self.number = number
685 self.project = project
686 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100687 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700688 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100689 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700690 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100691 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700692 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100693 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100694 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800695 self.reviews = []
696 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700697 self.updated_at = None
698 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100699 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100700 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700701 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700702 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100703 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700704 self._updateTimeStamp()
705
Jan Hruban570d01c2016-03-10 21:51:32 +0100706 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700707 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100708 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700709 self._updateTimeStamp()
710
Jan Hruban570d01c2016-03-10 21:51:32 +0100711 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700712 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100713 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700714 self._updateTimeStamp()
715
716 def getPullRequestOpenedEvent(self):
717 return self._getPullRequestEvent('opened')
718
719 def getPullRequestSynchronizeEvent(self):
720 return self._getPullRequestEvent('synchronize')
721
722 def getPullRequestReopenedEvent(self):
723 return self._getPullRequestEvent('reopened')
724
725 def getPullRequestClosedEvent(self):
726 return self._getPullRequestEvent('closed')
727
Jesse Keatinga41566f2017-06-14 18:17:51 -0700728 def getPullRequestEditedEvent(self):
729 return self._getPullRequestEvent('edited')
730
Gregory Haynes4fc12542015-04-22 20:38:06 -0700731 def addComment(self, message):
732 self.comments.append(message)
733 self._updateTimeStamp()
734
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200735 def getCommentAddedEvent(self, text):
736 name = 'issue_comment'
737 data = {
738 'action': 'created',
739 'issue': {
740 'number': self.number
741 },
742 'comment': {
743 'body': text
744 },
745 'repository': {
746 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100747 },
748 'sender': {
749 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200750 }
751 }
752 return (name, data)
753
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800754 def getReviewAddedEvent(self, review):
755 name = 'pull_request_review'
756 data = {
757 'action': 'submitted',
758 'pull_request': {
759 'number': self.number,
760 'title': self.subject,
761 'updated_at': self.updated_at,
762 'base': {
763 'ref': self.branch,
764 'repo': {
765 'full_name': self.project
766 }
767 },
768 'head': {
769 'sha': self.head_sha
770 }
771 },
772 'review': {
773 'state': review
774 },
775 'repository': {
776 'full_name': self.project
777 },
778 'sender': {
779 'login': 'ghuser'
780 }
781 }
782 return (name, data)
783
Jan Hruban16ad31f2015-11-07 14:39:07 +0100784 def addLabel(self, name):
785 if name not in self.labels:
786 self.labels.append(name)
787 self._updateTimeStamp()
788 return self._getLabelEvent(name)
789
790 def removeLabel(self, name):
791 if name in self.labels:
792 self.labels.remove(name)
793 self._updateTimeStamp()
794 return self._getUnlabelEvent(name)
795
796 def _getLabelEvent(self, label):
797 name = 'pull_request'
798 data = {
799 'action': 'labeled',
800 'pull_request': {
801 'number': self.number,
802 'updated_at': self.updated_at,
803 'base': {
804 'ref': self.branch,
805 'repo': {
806 'full_name': self.project
807 }
808 },
809 'head': {
810 'sha': self.head_sha
811 }
812 },
813 'label': {
814 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100815 },
816 'sender': {
817 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100818 }
819 }
820 return (name, data)
821
822 def _getUnlabelEvent(self, label):
823 name = 'pull_request'
824 data = {
825 'action': 'unlabeled',
826 'pull_request': {
827 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100828 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100829 'updated_at': self.updated_at,
830 'base': {
831 'ref': self.branch,
832 'repo': {
833 'full_name': self.project
834 }
835 },
836 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800837 'sha': self.head_sha,
838 'repo': {
839 'full_name': self.project
840 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100841 }
842 },
843 'label': {
844 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100845 },
846 'sender': {
847 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100848 }
849 }
850 return (name, data)
851
Jesse Keatinga41566f2017-06-14 18:17:51 -0700852 def editBody(self, body):
853 self.body = body
854 self._updateTimeStamp()
855
Gregory Haynes4fc12542015-04-22 20:38:06 -0700856 def _getRepo(self):
857 repo_path = os.path.join(self.upstream_root, self.project)
858 return git.Repo(repo_path)
859
860 def _createPRRef(self):
861 repo = self._getRepo()
862 GithubChangeReference.create(
863 repo, self._getPRReference(), 'refs/tags/init')
864
Jan Hruban570d01c2016-03-10 21:51:32 +0100865 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700866 repo = self._getRepo()
867 ref = repo.references[self._getPRReference()]
868 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100869 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700870 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100871 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700872 repo.head.reference = ref
873 zuul.merger.merger.reset_repo_to_head(repo)
874 repo.git.clean('-x', '-f', '-d')
875
Jan Hruban570d01c2016-03-10 21:51:32 +0100876 if files:
877 fn = files[0]
878 self.files = files
879 else:
880 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
881 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100882 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700883 fn = os.path.join(repo.working_dir, fn)
884 f = open(fn, 'w')
885 with open(fn, 'w') as f:
886 f.write("test %s %s\n" %
887 (self.branch, self.number))
888 repo.index.add([fn])
889
890 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800891 # Create an empty set of statuses for the given sha,
892 # each sha on a PR may have a status set on it
893 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700894 repo.head.reference = 'master'
895 zuul.merger.merger.reset_repo_to_head(repo)
896 repo.git.clean('-x', '-f', '-d')
897 repo.heads['master'].checkout()
898
899 def _updateTimeStamp(self):
900 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
901
902 def getPRHeadSha(self):
903 repo = self._getRepo()
904 return repo.references[self._getPRReference()].commit.hexsha
905
Jesse Keatingae4cd272017-01-30 17:10:44 -0800906 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800907 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
908 # convert the timestamp to a str format that would be returned
909 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800910
Adam Gandelmand81dd762017-02-09 15:15:49 -0800911 if granted_on:
912 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
913 submitted_at = time.strftime(
914 gh_time_format, granted_on.timetuple())
915 else:
916 # github timestamps only down to the second, so we need to make
917 # sure reviews that tests add appear to be added over a period of
918 # time in the past and not all at once.
919 if not self.reviews:
920 # the first review happens 10 mins ago
921 offset = 600
922 else:
923 # subsequent reviews happen 1 minute closer to now
924 offset = 600 - (len(self.reviews) * 60)
925
926 granted_on = datetime.datetime.utcfromtimestamp(
927 time.time() - offset)
928 submitted_at = time.strftime(
929 gh_time_format, granted_on.timetuple())
930
Jesse Keatingae4cd272017-01-30 17:10:44 -0800931 self.reviews.append({
932 'state': state,
933 'user': {
934 'login': user,
935 'email': user + "@derp.com",
936 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800937 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800938 })
939
Gregory Haynes4fc12542015-04-22 20:38:06 -0700940 def _getPRReference(self):
941 return '%s/head' % self.number
942
943 def _getPullRequestEvent(self, action):
944 name = 'pull_request'
945 data = {
946 'action': action,
947 'number': self.number,
948 'pull_request': {
949 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100950 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700951 'updated_at': self.updated_at,
952 'base': {
953 'ref': self.branch,
954 'repo': {
955 'full_name': self.project
956 }
957 },
958 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800959 'sha': self.head_sha,
960 'repo': {
961 'full_name': self.project
962 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700963 },
964 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100965 },
966 'sender': {
967 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 }
969 }
970 return (name, data)
971
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800972 def getCommitStatusEvent(self, context, state='success', user='zuul'):
973 name = 'status'
974 data = {
975 'state': state,
976 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700977 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800978 'description': 'Test results for %s: %s' % (self.head_sha, state),
979 'target_url': 'http://zuul/%s' % self.head_sha,
980 'branches': [],
981 'context': context,
982 'sender': {
983 'login': user
984 }
985 }
986 return (name, data)
987
James E. Blair289f5932017-07-27 15:02:29 -0700988 def setMerged(self, commit_message):
989 self.is_merged = True
990 self.merge_message = commit_message
991
992 repo = self._getRepo()
993 repo.heads[self.branch].commit = repo.commit(self.head_sha)
994
Gregory Haynes4fc12542015-04-22 20:38:06 -0700995
996class FakeGithubConnection(githubconnection.GithubConnection):
997 log = logging.getLogger("zuul.test.FakeGithubConnection")
998
999 def __init__(self, driver, connection_name, connection_config,
1000 upstream_root=None):
1001 super(FakeGithubConnection, self).__init__(driver, connection_name,
1002 connection_config)
1003 self.connection_name = connection_name
1004 self.pr_number = 0
1005 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001006 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001007 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001008 self.merge_failure = False
1009 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001010 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +02001011 self.github_client = FakeGithub()
1012
1013 def getGithubClient(self,
1014 project=None,
1015 user_id=None,
1016 use_app=True):
1017 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001018
Jesse Keatinga41566f2017-06-14 18:17:51 -07001019 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001020 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001021 self.pr_number += 1
1022 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001023 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001024 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001025 self.pull_requests.append(pull_request)
1026 return pull_request
1027
Jesse Keating71a47ff2017-06-06 11:36:43 -07001028 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1029 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001030 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001031 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001032 if not new_rev:
1033 new_rev = random_sha1()
1034 name = 'push'
1035 data = {
1036 'ref': ref,
1037 'before': old_rev,
1038 'after': new_rev,
1039 'repository': {
1040 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001041 },
1042 'commits': [
1043 {
1044 'added': added_files,
1045 'removed': removed_files,
1046 'modified': modified_files
1047 }
1048 ]
Wayne1a78c612015-06-11 17:14:13 -07001049 }
1050 return (name, data)
1051
Gregory Haynes4fc12542015-04-22 20:38:06 -07001052 def emitEvent(self, event):
1053 """Emulates sending the GitHub webhook event to the connection."""
1054 port = self.webapp.server.socket.getsockname()[1]
1055 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001056 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001057 secret = self.connection_config['webhook_token']
1058 signature = githubconnection._sign_request(payload, secret)
1059 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001060 req = urllib.request.Request(
1061 'http://localhost:%s/connection/%s/payload'
1062 % (port, self.connection_name),
1063 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001064 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001065
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001066 def addProject(self, project):
1067 # use the original method here and additionally register it in the
1068 # fake github
1069 super(FakeGithubConnection, self).addProject(project)
1070 self.getGithubClient(project).addProject(project)
1071
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001072 def getPull(self, project, number):
1073 pr = self.pull_requests[number - 1]
1074 data = {
1075 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001076 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001077 'updated_at': pr.updated_at,
1078 'base': {
1079 'repo': {
1080 'full_name': pr.project
1081 },
1082 'ref': pr.branch,
1083 },
Jan Hruban37615e52015-11-19 14:30:49 +01001084 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001085 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001086 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001087 'sha': pr.head_sha,
1088 'repo': {
1089 'full_name': pr.project
1090 }
Jesse Keating61040e72017-06-08 15:08:27 -07001091 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001092 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001093 'labels': pr.labels,
1094 'merged': pr.is_merged,
1095 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001096 }
1097 return data
1098
Jesse Keating9021a012017-08-29 14:45:27 -07001099 def getPullBySha(self, sha, project):
1100 prs = list(set([p for p in self.pull_requests if
1101 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001102 if len(prs) > 1:
1103 raise Exception('Multiple pulls found with head sha: %s' % sha)
1104 pr = prs[0]
1105 return self.getPull(pr.project, pr.number)
1106
Jesse Keatingae4cd272017-01-30 17:10:44 -08001107 def _getPullReviews(self, owner, project, number):
1108 pr = self.pull_requests[number - 1]
1109 return pr.reviews
1110
Jesse Keatingae4cd272017-01-30 17:10:44 -08001111 def getRepoPermission(self, project, login):
1112 owner, proj = project.split('/')
1113 for pr in self.pull_requests:
1114 pr_owner, pr_project = pr.project.split('/')
1115 if (pr_owner == owner and proj == pr_project):
1116 if login in pr.writers:
1117 return 'write'
1118 else:
1119 return 'read'
1120
Gregory Haynes4fc12542015-04-22 20:38:06 -07001121 def getGitUrl(self, project):
1122 return os.path.join(self.upstream_root, str(project))
1123
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001124 def real_getGitUrl(self, project):
1125 return super(FakeGithubConnection, self).getGitUrl(project)
1126
Jan Hrubane252a732017-01-03 15:03:09 +01001127 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001128 # record that this got reported
1129 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001130 pull_request = self.pull_requests[pr_number - 1]
1131 pull_request.addComment(message)
1132
Jan Hruban3b415922016-02-03 13:10:22 +01001133 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001134 # record that this got reported
1135 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001136 pull_request = self.pull_requests[pr_number - 1]
1137 if self.merge_failure:
1138 raise Exception('Pull request was not merged')
1139 if self.merge_not_allowed_count > 0:
1140 self.merge_not_allowed_count -= 1
1141 raise MergeFailure('Merge was not successful due to mergeability'
1142 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001143 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001144
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001145 def setCommitStatus(self, project, sha, state, url='', description='',
1146 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001147 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001148 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001149 super(FakeGithubConnection, self).setCommitStatus(
1150 project, sha, state,
1151 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001152
Jan Hruban16ad31f2015-11-07 14:39:07 +01001153 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001154 # record that this got reported
1155 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001156 pull_request = self.pull_requests[pr_number - 1]
1157 pull_request.addLabel(label)
1158
1159 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001160 # record that this got reported
1161 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001162 pull_request = self.pull_requests[pr_number - 1]
1163 pull_request.removeLabel(label)
1164
Jesse Keatinga41566f2017-06-14 18:17:51 -07001165 def _getNeededByFromPR(self, change):
1166 prs = []
1167 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001168 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001169 change.number))
1170 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001171 if not pr.body:
1172 body = ''
1173 else:
1174 body = pr.body
1175 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001176 # Get our version of a pull so that it's a dict
1177 pull = self.getPull(pr.project, pr.number)
1178 prs.append(pull)
1179
1180 return prs
1181
Gregory Haynes4fc12542015-04-22 20:38:06 -07001182
Clark Boylanb640e052014-04-03 16:41:46 -07001183class BuildHistory(object):
1184 def __init__(self, **kw):
1185 self.__dict__.update(kw)
1186
1187 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001188 return ("<Completed build, result: %s name: %s uuid: %s "
1189 "changes: %s ref: %s>" %
1190 (self.result, self.name, self.uuid,
1191 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001192
1193
Clark Boylanb640e052014-04-03 16:41:46 -07001194class FakeStatsd(threading.Thread):
1195 def __init__(self):
1196 threading.Thread.__init__(self)
1197 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001198 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001199 self.sock.bind(('', 0))
1200 self.port = self.sock.getsockname()[1]
1201 self.wake_read, self.wake_write = os.pipe()
1202 self.stats = []
1203
1204 def run(self):
1205 while True:
1206 poll = select.poll()
1207 poll.register(self.sock, select.POLLIN)
1208 poll.register(self.wake_read, select.POLLIN)
1209 ret = poll.poll()
1210 for (fd, event) in ret:
1211 if fd == self.sock.fileno():
1212 data = self.sock.recvfrom(1024)
1213 if not data:
1214 return
1215 self.stats.append(data[0])
1216 if fd == self.wake_read:
1217 return
1218
1219 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001220 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001221
1222
James E. Blaire1767bc2016-08-02 10:00:27 -07001223class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001224 log = logging.getLogger("zuul.test")
1225
Paul Belanger174a8272017-03-14 13:20:10 -04001226 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001227 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001228 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001229 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001230 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001231 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001232 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001233 # TODOv3(jeblair): self.node is really "the label of the node
1234 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001235 # keep using it like this, or we may end up exposing more of
1236 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001237 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001238 self.node = None
1239 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001240 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001241 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001242 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001243 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001244 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001245 self.wait_condition = threading.Condition()
1246 self.waiting = False
1247 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001248 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001249 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001250 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001251 items = self.parameters['zuul']['items']
1252 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1253 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001254
James E. Blair3158e282016-08-19 09:34:11 -07001255 def __repr__(self):
1256 waiting = ''
1257 if self.waiting:
1258 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001259 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1260 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001261
Clark Boylanb640e052014-04-03 16:41:46 -07001262 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001263 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001264 self.wait_condition.acquire()
1265 self.wait_condition.notify()
1266 self.waiting = False
1267 self.log.debug("Build %s released" % self.unique)
1268 self.wait_condition.release()
1269
1270 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001271 """Return whether this build is being held.
1272
1273 :returns: Whether the build is being held.
1274 :rtype: bool
1275 """
1276
Clark Boylanb640e052014-04-03 16:41:46 -07001277 self.wait_condition.acquire()
1278 if self.waiting:
1279 ret = True
1280 else:
1281 ret = False
1282 self.wait_condition.release()
1283 return ret
1284
1285 def _wait(self):
1286 self.wait_condition.acquire()
1287 self.waiting = True
1288 self.log.debug("Build %s waiting" % self.unique)
1289 self.wait_condition.wait()
1290 self.wait_condition.release()
1291
1292 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001293 self.log.debug('Running build %s' % self.unique)
1294
Paul Belanger174a8272017-03-14 13:20:10 -04001295 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001296 self.log.debug('Holding build %s' % self.unique)
1297 self._wait()
1298 self.log.debug("Build %s continuing" % self.unique)
1299
James E. Blair412fba82017-01-26 15:00:50 -08001300 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001301 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001302 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001303 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001304 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001305 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001306 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001307
James E. Blaire1767bc2016-08-02 10:00:27 -07001308 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001309
James E. Blaira5dba232016-08-08 15:53:24 -07001310 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001311 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001312 for change in changes:
1313 if self.hasChanges(change):
1314 return True
1315 return False
1316
James E. Blaire7b99a02016-08-05 14:27:34 -07001317 def hasChanges(self, *changes):
1318 """Return whether this build has certain changes in its git repos.
1319
1320 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001321 are expected to be present (in order) in the git repository of
1322 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001323
1324 :returns: Whether the build has the indicated changes.
1325 :rtype: bool
1326
1327 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001328 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001329 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001330 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001331 try:
1332 repo = git.Repo(path)
1333 except NoSuchPathError as e:
1334 self.log.debug('%s' % e)
1335 return False
James E. Blair247cab72017-07-20 16:52:36 -07001336 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001337 commit_message = '%s-1' % change.subject
1338 self.log.debug("Checking if build %s has changes; commit_message "
1339 "%s; repo_messages %s" % (self, commit_message,
1340 repo_messages))
1341 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001342 self.log.debug(" messages do not match")
1343 return False
1344 self.log.debug(" OK")
1345 return True
1346
James E. Blaird8af5422017-05-24 13:59:40 -07001347 def getWorkspaceRepos(self, projects):
1348 """Return workspace git repo objects for the listed projects
1349
1350 :arg list projects: A list of strings, each the canonical name
1351 of a project.
1352
1353 :returns: A dictionary of {name: repo} for every listed
1354 project.
1355 :rtype: dict
1356
1357 """
1358
1359 repos = {}
1360 for project in projects:
1361 path = os.path.join(self.jobdir.src_root, project)
1362 repo = git.Repo(path)
1363 repos[project] = repo
1364 return repos
1365
Clark Boylanb640e052014-04-03 16:41:46 -07001366
Paul Belanger174a8272017-03-14 13:20:10 -04001367class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1368 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001369
Paul Belanger174a8272017-03-14 13:20:10 -04001370 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001371 they will report that they have started but then pause until
1372 released before reporting completion. This attribute may be
1373 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001374 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001375 be explicitly released.
1376
1377 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001378 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001379 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001380 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001381 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001382 self.hold_jobs_in_build = False
1383 self.lock = threading.Lock()
1384 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001385 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001386 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001387 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001388
James E. Blaira5dba232016-08-08 15:53:24 -07001389 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001390 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001391
1392 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001393 :arg Change change: The :py:class:`~tests.base.FakeChange`
1394 instance which should cause the job to fail. This job
1395 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001396
1397 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001398 l = self.fail_tests.get(name, [])
1399 l.append(change)
1400 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001401
James E. Blair962220f2016-08-03 11:22:38 -07001402 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001403 """Release a held build.
1404
1405 :arg str regex: A regular expression which, if supplied, will
1406 cause only builds with matching names to be released. If
1407 not supplied, all builds will be released.
1408
1409 """
James E. Blair962220f2016-08-03 11:22:38 -07001410 builds = self.running_builds[:]
1411 self.log.debug("Releasing build %s (%s)" % (regex,
1412 len(self.running_builds)))
1413 for build in builds:
1414 if not regex or re.match(regex, build.name):
1415 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001416 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001417 build.release()
1418 else:
1419 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001420 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001421 self.log.debug("Done releasing builds %s (%s)" %
1422 (regex, len(self.running_builds)))
1423
Paul Belanger174a8272017-03-14 13:20:10 -04001424 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001425 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001426 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001427 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001428 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001429 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001430 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001431 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001432 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1433 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001434
1435 def stopJob(self, job):
1436 self.log.debug("handle stop")
1437 parameters = json.loads(job.arguments)
1438 uuid = parameters['uuid']
1439 for build in self.running_builds:
1440 if build.unique == uuid:
1441 build.aborted = True
1442 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001443 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001444
James E. Blaira002b032017-04-18 10:35:48 -07001445 def stop(self):
1446 for build in self.running_builds:
1447 build.release()
1448 super(RecordingExecutorServer, self).stop()
1449
Joshua Hesketh50c21782016-10-13 21:34:14 +11001450
Paul Belanger174a8272017-03-14 13:20:10 -04001451class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001452 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001453 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001454 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001455 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001456 if not commit: # merge conflict
1457 self.recordResult('MERGER_FAILURE')
1458 return commit
1459
1460 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001461 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001462 self.executor_server.lock.acquire()
1463 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001464 BuildHistory(name=build.name, result=result, changes=build.changes,
1465 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001466 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001467 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001468 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001469 )
Paul Belanger174a8272017-03-14 13:20:10 -04001470 self.executor_server.running_builds.remove(build)
1471 del self.executor_server.job_builds[self.job.unique]
1472 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001473
1474 def runPlaybooks(self, args):
1475 build = self.executor_server.job_builds[self.job.unique]
1476 build.jobdir = self.jobdir
1477
1478 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1479 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001480 return result
1481
James E. Blair892cca62017-08-09 11:36:58 -07001482 def runAnsible(self, cmd, timeout, playbook):
Paul Belanger174a8272017-03-14 13:20:10 -04001483 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001484
Paul Belanger174a8272017-03-14 13:20:10 -04001485 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001486 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair892cca62017-08-09 11:36:58 -07001487 cmd, timeout, playbook)
James E. Blair412fba82017-01-26 15:00:50 -08001488 else:
1489 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001490 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001491
James E. Blairad8dca02017-02-21 11:48:32 -05001492 def getHostList(self, args):
1493 self.log.debug("hostlist")
1494 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001495 for host in hosts:
1496 host['host_vars']['ansible_connection'] = 'local'
1497
1498 hosts.append(dict(
1499 name='localhost',
1500 host_vars=dict(ansible_connection='local'),
1501 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001502 return hosts
1503
James E. Blairf5dbd002015-12-23 15:26:17 -08001504
Clark Boylanb640e052014-04-03 16:41:46 -07001505class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001506 """A Gearman server for use in tests.
1507
1508 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1509 added to the queue but will not be distributed to workers
1510 until released. This attribute may be changed at any time and
1511 will take effect for subsequently enqueued jobs, but
1512 previously held jobs will still need to be explicitly
1513 released.
1514
1515 """
1516
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001517 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001518 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001519 if use_ssl:
1520 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1521 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1522 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1523 else:
1524 ssl_ca = None
1525 ssl_cert = None
1526 ssl_key = None
1527
1528 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1529 ssl_cert=ssl_cert,
1530 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001531
1532 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001533 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1534 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001535 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001536 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001537 job.waiting = self.hold_jobs_in_queue
1538 else:
1539 job.waiting = False
1540 if job.waiting:
1541 continue
1542 if job.name in connection.functions:
1543 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001544 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001545 connection.related_jobs[job.handle] = job
1546 job.worker_connection = connection
1547 job.running = True
1548 return job
1549 return None
1550
1551 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001552 """Release a held job.
1553
1554 :arg str regex: A regular expression which, if supplied, will
1555 cause only jobs with matching names to be released. If
1556 not supplied, all jobs will be released.
1557 """
Clark Boylanb640e052014-04-03 16:41:46 -07001558 released = False
1559 qlen = (len(self.high_queue) + len(self.normal_queue) +
1560 len(self.low_queue))
1561 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1562 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001563 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001564 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001565 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001566 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001567 self.log.debug("releasing queued job %s" %
1568 job.unique)
1569 job.waiting = False
1570 released = True
1571 else:
1572 self.log.debug("not releasing queued job %s" %
1573 job.unique)
1574 if released:
1575 self.wakeConnections()
1576 qlen = (len(self.high_queue) + len(self.normal_queue) +
1577 len(self.low_queue))
1578 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1579
1580
1581class FakeSMTP(object):
1582 log = logging.getLogger('zuul.FakeSMTP')
1583
1584 def __init__(self, messages, server, port):
1585 self.server = server
1586 self.port = port
1587 self.messages = messages
1588
1589 def sendmail(self, from_email, to_email, msg):
1590 self.log.info("Sending email from %s, to %s, with msg %s" % (
1591 from_email, to_email, msg))
1592
1593 headers = msg.split('\n\n', 1)[0]
1594 body = msg.split('\n\n', 1)[1]
1595
1596 self.messages.append(dict(
1597 from_email=from_email,
1598 to_email=to_email,
1599 msg=msg,
1600 headers=headers,
1601 body=body,
1602 ))
1603
1604 return True
1605
1606 def quit(self):
1607 return True
1608
1609
James E. Blairdce6cea2016-12-20 16:45:32 -08001610class FakeNodepool(object):
1611 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001612 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001613
1614 log = logging.getLogger("zuul.test.FakeNodepool")
1615
1616 def __init__(self, host, port, chroot):
1617 self.client = kazoo.client.KazooClient(
1618 hosts='%s:%s%s' % (host, port, chroot))
1619 self.client.start()
1620 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001621 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001622 self.thread = threading.Thread(target=self.run)
1623 self.thread.daemon = True
1624 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001625 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001626
1627 def stop(self):
1628 self._running = False
1629 self.thread.join()
1630 self.client.stop()
1631 self.client.close()
1632
1633 def run(self):
1634 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001635 try:
1636 self._run()
1637 except Exception:
1638 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001639 time.sleep(0.1)
1640
1641 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001642 if self.paused:
1643 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001644 for req in self.getNodeRequests():
1645 self.fulfillRequest(req)
1646
1647 def getNodeRequests(self):
1648 try:
1649 reqids = self.client.get_children(self.REQUEST_ROOT)
1650 except kazoo.exceptions.NoNodeError:
1651 return []
1652 reqs = []
1653 for oid in sorted(reqids):
1654 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001655 try:
1656 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001657 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001658 data['_oid'] = oid
1659 reqs.append(data)
1660 except kazoo.exceptions.NoNodeError:
1661 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001662 return reqs
1663
James E. Blaire18d4602017-01-05 11:17:28 -08001664 def getNodes(self):
1665 try:
1666 nodeids = self.client.get_children(self.NODE_ROOT)
1667 except kazoo.exceptions.NoNodeError:
1668 return []
1669 nodes = []
1670 for oid in sorted(nodeids):
1671 path = self.NODE_ROOT + '/' + oid
1672 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001673 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001674 data['_oid'] = oid
1675 try:
1676 lockfiles = self.client.get_children(path + '/lock')
1677 except kazoo.exceptions.NoNodeError:
1678 lockfiles = []
1679 if lockfiles:
1680 data['_lock'] = True
1681 else:
1682 data['_lock'] = False
1683 nodes.append(data)
1684 return nodes
1685
James E. Blaira38c28e2017-01-04 10:33:20 -08001686 def makeNode(self, request_id, node_type):
1687 now = time.time()
1688 path = '/nodepool/nodes/'
1689 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001690 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001691 provider='test-provider',
1692 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001693 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001694 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001695 public_ipv4='127.0.0.1',
1696 private_ipv4=None,
1697 public_ipv6=None,
1698 allocated_to=request_id,
1699 state='ready',
1700 state_time=now,
1701 created_time=now,
1702 updated_time=now,
1703 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001704 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001705 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001706 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001707 path = self.client.create(path, data,
1708 makepath=True,
1709 sequence=True)
1710 nodeid = path.split("/")[-1]
1711 return nodeid
1712
James E. Blair6ab79e02017-01-06 10:10:17 -08001713 def addFailRequest(self, request):
1714 self.fail_requests.add(request['_oid'])
1715
James E. Blairdce6cea2016-12-20 16:45:32 -08001716 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001717 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001718 return
1719 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001720 oid = request['_oid']
1721 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001722
James E. Blair6ab79e02017-01-06 10:10:17 -08001723 if oid in self.fail_requests:
1724 request['state'] = 'failed'
1725 else:
1726 request['state'] = 'fulfilled'
1727 nodes = []
1728 for node in request['node_types']:
1729 nodeid = self.makeNode(oid, node)
1730 nodes.append(nodeid)
1731 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001732
James E. Blaira38c28e2017-01-04 10:33:20 -08001733 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001734 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001735 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001736 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001737 try:
1738 self.client.set(path, data)
1739 except kazoo.exceptions.NoNodeError:
1740 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001741
1742
James E. Blair498059b2016-12-20 13:50:13 -08001743class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001744 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001745 super(ChrootedKazooFixture, self).__init__()
1746
1747 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1748 if ':' in zk_host:
1749 host, port = zk_host.split(':')
1750 else:
1751 host = zk_host
1752 port = None
1753
1754 self.zookeeper_host = host
1755
1756 if not port:
1757 self.zookeeper_port = 2181
1758 else:
1759 self.zookeeper_port = int(port)
1760
Clark Boylan621ec9a2017-04-07 17:41:33 -07001761 self.test_id = test_id
1762
James E. Blair498059b2016-12-20 13:50:13 -08001763 def _setUp(self):
1764 # Make sure the test chroot paths do not conflict
1765 random_bits = ''.join(random.choice(string.ascii_lowercase +
1766 string.ascii_uppercase)
1767 for x in range(8))
1768
Clark Boylan621ec9a2017-04-07 17:41:33 -07001769 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001770 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1771
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001772 self.addCleanup(self._cleanup)
1773
James E. Blair498059b2016-12-20 13:50:13 -08001774 # Ensure the chroot path exists and clean up any pre-existing znodes.
1775 _tmp_client = kazoo.client.KazooClient(
1776 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1777 _tmp_client.start()
1778
1779 if _tmp_client.exists(self.zookeeper_chroot):
1780 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1781
1782 _tmp_client.ensure_path(self.zookeeper_chroot)
1783 _tmp_client.stop()
1784 _tmp_client.close()
1785
James E. Blair498059b2016-12-20 13:50:13 -08001786 def _cleanup(self):
1787 '''Remove the chroot path.'''
1788 # Need a non-chroot'ed client to remove the chroot path
1789 _tmp_client = kazoo.client.KazooClient(
1790 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1791 _tmp_client.start()
1792 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1793 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001794 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001795
1796
Joshua Heskethd78b4482015-09-14 16:56:34 -06001797class MySQLSchemaFixture(fixtures.Fixture):
1798 def setUp(self):
1799 super(MySQLSchemaFixture, self).setUp()
1800
1801 random_bits = ''.join(random.choice(string.ascii_lowercase +
1802 string.ascii_uppercase)
1803 for x in range(8))
1804 self.name = '%s_%s' % (random_bits, os.getpid())
1805 self.passwd = uuid.uuid4().hex
1806 db = pymysql.connect(host="localhost",
1807 user="openstack_citest",
1808 passwd="openstack_citest",
1809 db="openstack_citest")
1810 cur = db.cursor()
1811 cur.execute("create database %s" % self.name)
1812 cur.execute(
1813 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1814 (self.name, self.name, self.passwd))
1815 cur.execute("flush privileges")
1816
1817 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1818 self.passwd,
1819 self.name)
1820 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1821 self.addCleanup(self.cleanup)
1822
1823 def cleanup(self):
1824 db = pymysql.connect(host="localhost",
1825 user="openstack_citest",
1826 passwd="openstack_citest",
1827 db="openstack_citest")
1828 cur = db.cursor()
1829 cur.execute("drop database %s" % self.name)
1830 cur.execute("drop user '%s'@'localhost'" % self.name)
1831 cur.execute("flush privileges")
1832
1833
Maru Newby3fe5f852015-01-13 04:22:14 +00001834class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001835 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001836 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001837
James E. Blair1c236df2017-02-01 14:07:24 -08001838 def attachLogs(self, *args):
1839 def reader():
1840 self._log_stream.seek(0)
1841 while True:
1842 x = self._log_stream.read(4096)
1843 if not x:
1844 break
1845 yield x.encode('utf8')
1846 content = testtools.content.content_from_reader(
1847 reader,
1848 testtools.content_type.UTF8_TEXT,
1849 False)
1850 self.addDetail('logging', content)
1851
Clark Boylanb640e052014-04-03 16:41:46 -07001852 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001853 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001854 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1855 try:
1856 test_timeout = int(test_timeout)
1857 except ValueError:
1858 # If timeout value is invalid do not set a timeout.
1859 test_timeout = 0
1860 if test_timeout > 0:
1861 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1862
1863 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1864 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1865 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1866 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1867 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1868 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1869 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1870 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1871 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1872 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001873 self._log_stream = StringIO()
1874 self.addOnException(self.attachLogs)
1875 else:
1876 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001877
James E. Blair73b41772017-05-22 13:22:55 -07001878 # NOTE(jeblair): this is temporary extra debugging to try to
1879 # track down a possible leak.
1880 orig_git_repo_init = git.Repo.__init__
1881
1882 def git_repo_init(myself, *args, **kw):
1883 orig_git_repo_init(myself, *args, **kw)
1884 self.log.debug("Created git repo 0x%x %s" %
1885 (id(myself), repr(myself)))
1886
1887 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1888 git_repo_init))
1889
James E. Blair1c236df2017-02-01 14:07:24 -08001890 handler = logging.StreamHandler(self._log_stream)
1891 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1892 '%(levelname)-8s %(message)s')
1893 handler.setFormatter(formatter)
1894
1895 logger = logging.getLogger()
1896 logger.setLevel(logging.DEBUG)
1897 logger.addHandler(handler)
1898
Clark Boylan3410d532017-04-25 12:35:29 -07001899 # Make sure we don't carry old handlers around in process state
1900 # which slows down test runs
1901 self.addCleanup(logger.removeHandler, handler)
1902 self.addCleanup(handler.close)
1903 self.addCleanup(handler.flush)
1904
James E. Blair1c236df2017-02-01 14:07:24 -08001905 # NOTE(notmorgan): Extract logging overrides for specific
1906 # libraries from the OS_LOG_DEFAULTS env and create loggers
1907 # for each. This is used to limit the output during test runs
1908 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001909 log_defaults_from_env = os.environ.get(
1910 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001911 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001912
James E. Blairdce6cea2016-12-20 16:45:32 -08001913 if log_defaults_from_env:
1914 for default in log_defaults_from_env.split(','):
1915 try:
1916 name, level_str = default.split('=', 1)
1917 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001918 logger = logging.getLogger(name)
1919 logger.setLevel(level)
1920 logger.addHandler(handler)
1921 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001922 except ValueError:
1923 # NOTE(notmorgan): Invalid format of the log default,
1924 # skip and don't try and apply a logger for the
1925 # specified module
1926 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001927
Maru Newby3fe5f852015-01-13 04:22:14 +00001928
1929class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001930 """A test case with a functioning Zuul.
1931
1932 The following class variables are used during test setup and can
1933 be overidden by subclasses but are effectively read-only once a
1934 test method starts running:
1935
1936 :cvar str config_file: This points to the main zuul config file
1937 within the fixtures directory. Subclasses may override this
1938 to obtain a different behavior.
1939
1940 :cvar str tenant_config_file: This is the tenant config file
1941 (which specifies from what git repos the configuration should
1942 be loaded). It defaults to the value specified in
1943 `config_file` but can be overidden by subclasses to obtain a
1944 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001945 configuration. See also the :py:func:`simple_layout`
1946 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001947
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001948 :cvar bool create_project_keys: Indicates whether Zuul should
1949 auto-generate keys for each project, or whether the test
1950 infrastructure should insert dummy keys to save time during
1951 startup. Defaults to False.
1952
James E. Blaire7b99a02016-08-05 14:27:34 -07001953 The following are instance variables that are useful within test
1954 methods:
1955
1956 :ivar FakeGerritConnection fake_<connection>:
1957 A :py:class:`~tests.base.FakeGerritConnection` will be
1958 instantiated for each connection present in the config file
1959 and stored here. For instance, `fake_gerrit` will hold the
1960 FakeGerritConnection object for a connection named `gerrit`.
1961
1962 :ivar FakeGearmanServer gearman_server: An instance of
1963 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1964 server that all of the Zuul components in this test use to
1965 communicate with each other.
1966
Paul Belanger174a8272017-03-14 13:20:10 -04001967 :ivar RecordingExecutorServer executor_server: An instance of
1968 :py:class:`~tests.base.RecordingExecutorServer` which is the
1969 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001970
1971 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1972 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001973 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001974 list upon completion.
1975
1976 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1977 objects representing completed builds. They are appended to
1978 the list in the order they complete.
1979
1980 """
1981
James E. Blair83005782015-12-11 14:46:03 -08001982 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001983 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001984 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001985 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001986
1987 def _startMerger(self):
1988 self.merge_server = zuul.merger.server.MergeServer(self.config,
1989 self.connections)
1990 self.merge_server.start()
1991
Maru Newby3fe5f852015-01-13 04:22:14 +00001992 def setUp(self):
1993 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001994
1995 self.setupZK()
1996
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001997 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001998 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001999 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2000 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002001 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002002 tmp_root = tempfile.mkdtemp(
2003 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002004 self.test_root = os.path.join(tmp_root, "zuul-test")
2005 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002006 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002007 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002008 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002009 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2010 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002011
2012 if os.path.exists(self.test_root):
2013 shutil.rmtree(self.test_root)
2014 os.makedirs(self.test_root)
2015 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002016 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002017 os.makedirs(self.merger_state_root)
2018 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002019
2020 # Make per test copy of Configuration.
2021 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002022 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2023 if not os.path.exists(self.private_key_file):
2024 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2025 shutil.copy(src_private_key_file, self.private_key_file)
2026 shutil.copy('{}.pub'.format(src_private_key_file),
2027 '{}.pub'.format(self.private_key_file))
2028 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002029 self.config.set('scheduler', 'tenant_config',
2030 os.path.join(
2031 FIXTURE_DIR,
2032 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002033 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002034 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002035 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002036 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002037 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002038
Clark Boylanb640e052014-04-03 16:41:46 -07002039 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10002040 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
2041 # see: https://github.com/jsocol/pystatsd/issues/61
2042 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07002043 os.environ['STATSD_PORT'] = str(self.statsd.port)
2044 self.statsd.start()
2045 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05002046 importlib.reload(statsd)
2047 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07002048
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002049 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002050
2051 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002052 self.log.info("Gearman server on port %s" %
2053 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002054 if self.use_ssl:
2055 self.log.info('SSL enabled for gearman')
2056 self.config.set(
2057 'gearman', 'ssl_ca',
2058 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2059 self.config.set(
2060 'gearman', 'ssl_cert',
2061 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2062 self.config.set(
2063 'gearman', 'ssl_key',
2064 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002065
James E. Blaire511d2f2016-12-08 15:22:26 -08002066 gerritsource.GerritSource.replication_timeout = 1.5
2067 gerritsource.GerritSource.replication_retry_interval = 0.5
2068 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002069
Joshua Hesketh352264b2015-08-11 23:42:08 +10002070 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07002071
Jan Hruban7083edd2015-08-21 14:00:54 +02002072 self.webapp = zuul.webapp.WebApp(
2073 self.sched, port=0, listen_address='127.0.0.1')
2074
Jan Hruban6b71aff2015-10-22 16:58:08 +02002075 self.event_queues = [
2076 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002077 self.sched.trigger_event_queue,
2078 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002079 ]
2080
James E. Blairfef78942016-03-11 16:28:56 -08002081 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002082 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002083
Paul Belanger174a8272017-03-14 13:20:10 -04002084 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002085 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002086 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002087 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002088 _test_root=self.test_root,
2089 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002090 self.executor_server.start()
2091 self.history = self.executor_server.build_history
2092 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002093
Paul Belanger174a8272017-03-14 13:20:10 -04002094 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002095 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002096 self.merge_client = zuul.merger.client.MergeClient(
2097 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002098 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002099 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002100 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002101
James E. Blair0d5a36e2017-02-21 10:53:44 -05002102 self.fake_nodepool = FakeNodepool(
2103 self.zk_chroot_fixture.zookeeper_host,
2104 self.zk_chroot_fixture.zookeeper_port,
2105 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002106
Paul Belanger174a8272017-03-14 13:20:10 -04002107 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002108 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002109 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002110 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002111
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002112 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002113
2114 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002115 self.webapp.start()
2116 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002117 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002118 # Cleanups are run in reverse order
2119 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002120 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002121 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002122
James E. Blairb9c0d772017-03-03 14:34:49 -08002123 self.sched.reconfigure(self.config)
2124 self.sched.resume()
2125
Tobias Henkel7df274b2017-05-26 17:41:11 +02002126 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002127 # Set up gerrit related fakes
2128 # Set a changes database so multiple FakeGerrit's can report back to
2129 # a virtual canonical database given by the configured hostname
2130 self.gerrit_changes_dbs = {}
2131
2132 def getGerritConnection(driver, name, config):
2133 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2134 con = FakeGerritConnection(driver, name, config,
2135 changes_db=db,
2136 upstream_root=self.upstream_root)
2137 self.event_queues.append(con.event_queue)
2138 setattr(self, 'fake_' + name, con)
2139 return con
2140
2141 self.useFixture(fixtures.MonkeyPatch(
2142 'zuul.driver.gerrit.GerritDriver.getConnection',
2143 getGerritConnection))
2144
Gregory Haynes4fc12542015-04-22 20:38:06 -07002145 def getGithubConnection(driver, name, config):
2146 con = FakeGithubConnection(driver, name, config,
2147 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002148 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002149 setattr(self, 'fake_' + name, con)
2150 return con
2151
2152 self.useFixture(fixtures.MonkeyPatch(
2153 'zuul.driver.github.GithubDriver.getConnection',
2154 getGithubConnection))
2155
James E. Blaire511d2f2016-12-08 15:22:26 -08002156 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002157 # TODO(jhesketh): This should come from lib.connections for better
2158 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002159 # Register connections from the config
2160 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002161
Joshua Hesketh352264b2015-08-11 23:42:08 +10002162 def FakeSMTPFactory(*args, **kw):
2163 args = [self.smtp_messages] + list(args)
2164 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002165
Joshua Hesketh352264b2015-08-11 23:42:08 +10002166 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002167
James E. Blaire511d2f2016-12-08 15:22:26 -08002168 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002169 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002170 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002171
James E. Blair83005782015-12-11 14:46:03 -08002172 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002173 # This creates the per-test configuration object. It can be
2174 # overriden by subclasses, but should not need to be since it
2175 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002176 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002177 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002178
James E. Blair39840362017-06-23 20:34:02 +01002179 sections = ['zuul', 'scheduler', 'executor', 'merger']
2180 for section in sections:
2181 if not self.config.has_section(section):
2182 self.config.add_section(section)
2183
James E. Blair06cc3922017-04-19 10:08:10 -07002184 if not self.setupSimpleLayout():
2185 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002186 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002187 self.tenant_config_file)
2188 git_path = os.path.join(
2189 os.path.dirname(
2190 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2191 'git')
2192 if os.path.exists(git_path):
2193 for reponame in os.listdir(git_path):
2194 project = reponame.replace('_', '/')
2195 self.copyDirToRepo(project,
2196 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002197 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002198 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002199 self.setupAllProjectKeys()
2200
James E. Blair06cc3922017-04-19 10:08:10 -07002201 def setupSimpleLayout(self):
2202 # If the test method has been decorated with a simple_layout,
2203 # use that instead of the class tenant_config_file. Set up a
2204 # single config-project with the specified layout, and
2205 # initialize repos for all of the 'project' entries which
2206 # appear in the layout.
2207 test_name = self.id().split('.')[-1]
2208 test = getattr(self, test_name)
2209 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002210 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002211 else:
2212 return False
2213
James E. Blairb70e55a2017-04-19 12:57:02 -07002214 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002215 path = os.path.join(FIXTURE_DIR, path)
2216 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002217 data = f.read()
2218 layout = yaml.safe_load(data)
2219 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002220 untrusted_projects = []
2221 for item in layout:
2222 if 'project' in item:
2223 name = item['project']['name']
2224 untrusted_projects.append(name)
2225 self.init_repo(name)
2226 self.addCommitToRepo(name, 'initial commit',
2227 files={'README': ''},
2228 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002229 if 'job' in item:
2230 jobname = item['job']['name']
2231 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002232
2233 root = os.path.join(self.test_root, "config")
2234 if not os.path.exists(root):
2235 os.makedirs(root)
2236 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2237 config = [{'tenant':
2238 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002239 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002240 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002241 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002242 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002243 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002244 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002245 os.path.join(FIXTURE_DIR, f.name))
2246
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002247 self.init_repo('org/common-config')
2248 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002249 files, branch='master', tag='init')
2250
2251 return True
2252
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002253 def setupAllProjectKeys(self):
2254 if self.create_project_keys:
2255 return
2256
James E. Blair39840362017-06-23 20:34:02 +01002257 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002258 with open(os.path.join(FIXTURE_DIR, path)) as f:
2259 tenant_config = yaml.safe_load(f.read())
2260 for tenant in tenant_config:
2261 sources = tenant['tenant']['source']
2262 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002263 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002264 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002265 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002266 self.setupProjectKeys(source, project)
2267
2268 def setupProjectKeys(self, source, project):
2269 # Make sure we set up an RSA key for the project so that we
2270 # don't spend time generating one:
2271
James E. Blair6459db12017-06-29 14:57:20 -07002272 if isinstance(project, dict):
2273 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002274 key_root = os.path.join(self.state_root, 'keys')
2275 if not os.path.isdir(key_root):
2276 os.mkdir(key_root, 0o700)
2277 private_key_file = os.path.join(key_root, source, project + '.pem')
2278 private_key_dir = os.path.dirname(private_key_file)
2279 self.log.debug("Installing test keys for project %s at %s" % (
2280 project, private_key_file))
2281 if not os.path.isdir(private_key_dir):
2282 os.makedirs(private_key_dir)
2283 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2284 with open(private_key_file, 'w') as o:
2285 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002286
James E. Blair498059b2016-12-20 13:50:13 -08002287 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002288 self.zk_chroot_fixture = self.useFixture(
2289 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002290 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002291 self.zk_chroot_fixture.zookeeper_host,
2292 self.zk_chroot_fixture.zookeeper_port,
2293 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002294
James E. Blair96c6bf82016-01-15 16:20:40 -08002295 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002296 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002297
2298 files = {}
2299 for (dirpath, dirnames, filenames) in os.walk(source_path):
2300 for filename in filenames:
2301 test_tree_filepath = os.path.join(dirpath, filename)
2302 common_path = os.path.commonprefix([test_tree_filepath,
2303 source_path])
2304 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2305 with open(test_tree_filepath, 'r') as f:
2306 content = f.read()
2307 files[relative_filepath] = content
2308 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002309 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002310
James E. Blaire18d4602017-01-05 11:17:28 -08002311 def assertNodepoolState(self):
2312 # Make sure that there are no pending requests
2313
2314 requests = self.fake_nodepool.getNodeRequests()
2315 self.assertEqual(len(requests), 0)
2316
2317 nodes = self.fake_nodepool.getNodes()
2318 for node in nodes:
2319 self.assertFalse(node['_lock'], "Node %s is locked" %
2320 (node['_oid'],))
2321
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002322 def assertNoGeneratedKeys(self):
2323 # Make sure that Zuul did not generate any project keys
2324 # (unless it was supposed to).
2325
2326 if self.create_project_keys:
2327 return
2328
2329 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2330 test_key = i.read()
2331
2332 key_root = os.path.join(self.state_root, 'keys')
2333 for root, dirname, files in os.walk(key_root):
2334 for fn in files:
2335 with open(os.path.join(root, fn)) as f:
2336 self.assertEqual(test_key, f.read())
2337
Clark Boylanb640e052014-04-03 16:41:46 -07002338 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002339 self.log.debug("Assert final state")
2340 # Make sure no jobs are running
2341 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002342 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002343 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002344 gc.collect()
2345 for obj in gc.get_objects():
2346 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002347 self.log.debug("Leaked git repo object: 0x%x %s" %
2348 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002349 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002350 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002351 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002352 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002353 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002354 for tenant in self.sched.abide.tenants.values():
2355 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002356 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002357 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002358
2359 def shutdown(self):
2360 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002361 self.executor_server.hold_jobs_in_build = False
2362 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002363 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002364 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002365 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002366 self.sched.stop()
2367 self.sched.join()
2368 self.statsd.stop()
2369 self.statsd.join()
2370 self.webapp.stop()
2371 self.webapp.join()
2372 self.rpc.stop()
2373 self.rpc.join()
2374 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002375 self.fake_nodepool.stop()
2376 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002377 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002378 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002379 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002380 # Further the pydevd threads also need to be whitelisted so debugging
2381 # e.g. in PyCharm is possible without breaking shutdown.
2382 whitelist = ['executor-watchdog',
2383 'pydevd.CommandThread',
2384 'pydevd.Reader',
2385 'pydevd.Writer',
2386 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002387 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002388 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002389 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002390 log_str = ""
2391 for thread_id, stack_frame in sys._current_frames().items():
2392 log_str += "Thread: %s\n" % thread_id
2393 log_str += "".join(traceback.format_stack(stack_frame))
2394 self.log.debug(log_str)
2395 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002396
James E. Blaira002b032017-04-18 10:35:48 -07002397 def assertCleanShutdown(self):
2398 pass
2399
James E. Blairc4ba97a2017-04-19 16:26:24 -07002400 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002401 parts = project.split('/')
2402 path = os.path.join(self.upstream_root, *parts[:-1])
2403 if not os.path.exists(path):
2404 os.makedirs(path)
2405 path = os.path.join(self.upstream_root, project)
2406 repo = git.Repo.init(path)
2407
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002408 with repo.config_writer() as config_writer:
2409 config_writer.set_value('user', 'email', 'user@example.com')
2410 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002411
Clark Boylanb640e052014-04-03 16:41:46 -07002412 repo.index.commit('initial commit')
2413 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002414 if tag:
2415 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002416
James E. Blair97d902e2014-08-21 13:25:56 -07002417 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002418 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002419 repo.git.clean('-x', '-f', '-d')
2420
James E. Blair97d902e2014-08-21 13:25:56 -07002421 def create_branch(self, project, branch):
2422 path = os.path.join(self.upstream_root, project)
2423 repo = git.Repo.init(path)
2424 fn = os.path.join(path, 'README')
2425
2426 branch_head = repo.create_head(branch)
2427 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002428 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002429 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002430 f.close()
2431 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002432 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002433
James E. Blair97d902e2014-08-21 13:25:56 -07002434 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002435 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002436 repo.git.clean('-x', '-f', '-d')
2437
Sachi King9f16d522016-03-16 12:20:45 +11002438 def create_commit(self, project):
2439 path = os.path.join(self.upstream_root, project)
2440 repo = git.Repo(path)
2441 repo.head.reference = repo.heads['master']
2442 file_name = os.path.join(path, 'README')
2443 with open(file_name, 'a') as f:
2444 f.write('creating fake commit\n')
2445 repo.index.add([file_name])
2446 commit = repo.index.commit('Creating a fake commit')
2447 return commit.hexsha
2448
James E. Blairf4a5f022017-04-18 14:01:10 -07002449 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002450 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002451 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002452 while len(self.builds):
2453 self.release(self.builds[0])
2454 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002455 i += 1
2456 if count is not None and i >= count:
2457 break
James E. Blairb8c16472015-05-05 14:55:26 -07002458
James E. Blairdf25ddc2017-07-08 07:57:09 -07002459 def getSortedBuilds(self):
2460 "Return the list of currently running builds sorted by name"
2461
2462 return sorted(self.builds, key=lambda x: x.name)
2463
Clark Boylanb640e052014-04-03 16:41:46 -07002464 def release(self, job):
2465 if isinstance(job, FakeBuild):
2466 job.release()
2467 else:
2468 job.waiting = False
2469 self.log.debug("Queued job %s released" % job.unique)
2470 self.gearman_server.wakeConnections()
2471
2472 def getParameter(self, job, name):
2473 if isinstance(job, FakeBuild):
2474 return job.parameters[name]
2475 else:
2476 parameters = json.loads(job.arguments)
2477 return parameters[name]
2478
Clark Boylanb640e052014-04-03 16:41:46 -07002479 def haveAllBuildsReported(self):
2480 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002481 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002482 return False
2483 # Find out if every build that the worker has completed has been
2484 # reported back to Zuul. If it hasn't then that means a Gearman
2485 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002486 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002487 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002488 if not zbuild:
2489 # It has already been reported
2490 continue
2491 # It hasn't been reported yet.
2492 return False
2493 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002494 worker = self.executor_server.executor_worker
2495 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002496 if connection.state == 'GRAB_WAIT':
2497 return False
2498 return True
2499
2500 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002501 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002502 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002503 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002504 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002505 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002506 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002507 for j in conn.related_jobs.values():
2508 if j.unique == build.uuid:
2509 client_job = j
2510 break
2511 if not client_job:
2512 self.log.debug("%s is not known to the gearman client" %
2513 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002514 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002515 if not client_job.handle:
2516 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002517 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002518 server_job = self.gearman_server.jobs.get(client_job.handle)
2519 if not server_job:
2520 self.log.debug("%s is not known to the gearman server" %
2521 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002522 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002523 if not hasattr(server_job, 'waiting'):
2524 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002525 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002526 if server_job.waiting:
2527 continue
James E. Blair17302972016-08-10 16:11:42 -07002528 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002529 self.log.debug("%s has not reported start" % build)
2530 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002531 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002532 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002533 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002534 if worker_build:
2535 if worker_build.isWaiting():
2536 continue
2537 else:
2538 self.log.debug("%s is running" % worker_build)
2539 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002540 else:
James E. Blair962220f2016-08-03 11:22:38 -07002541 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002542 return False
James E. Blaira002b032017-04-18 10:35:48 -07002543 for (build_uuid, job_worker) in \
2544 self.executor_server.job_workers.items():
2545 if build_uuid not in seen_builds:
2546 self.log.debug("%s is not finalized" % build_uuid)
2547 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002548 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002549
James E. Blairdce6cea2016-12-20 16:45:32 -08002550 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002551 if self.fake_nodepool.paused:
2552 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002553 if self.sched.nodepool.requests:
2554 return False
2555 return True
2556
Jan Hruban6b71aff2015-10-22 16:58:08 +02002557 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002558 for event_queue in self.event_queues:
2559 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002560
2561 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002562 for event_queue in self.event_queues:
2563 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002564
Clark Boylanb640e052014-04-03 16:41:46 -07002565 def waitUntilSettled(self):
2566 self.log.debug("Waiting until settled...")
2567 start = time.time()
2568 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002569 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002570 self.log.error("Timeout waiting for Zuul to settle")
2571 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002572 for event_queue in self.event_queues:
2573 self.log.error(" %s: %s" %
2574 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002575 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002576 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002577 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002578 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002579 self.log.error("All requests completed: %s" %
2580 (self.areAllNodeRequestsComplete(),))
2581 self.log.error("Merge client jobs: %s" %
2582 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002583 raise Exception("Timeout waiting for Zuul to settle")
2584 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002585
Paul Belanger174a8272017-03-14 13:20:10 -04002586 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002587 # have all build states propogated to zuul?
2588 if self.haveAllBuildsReported():
2589 # Join ensures that the queue is empty _and_ events have been
2590 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002591 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002592 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002593 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002594 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002595 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002596 self.areAllNodeRequestsComplete() and
2597 all(self.eventQueuesEmpty())):
2598 # The queue empty check is placed at the end to
2599 # ensure that if a component adds an event between
2600 # when locked the run handler and checked that the
2601 # components were stable, we don't erroneously
2602 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002603 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002604 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002605 self.log.debug("...settled.")
2606 return
2607 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002608 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002609 self.sched.wake_event.wait(0.1)
2610
2611 def countJobResults(self, jobs, result):
2612 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002613 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002614
Monty Taylor0d926122017-05-24 08:07:56 -05002615 def getBuildByName(self, name):
2616 for build in self.builds:
2617 if build.name == name:
2618 return build
2619 raise Exception("Unable to find build %s" % name)
2620
James E. Blair96c6bf82016-01-15 16:20:40 -08002621 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002622 for job in self.history:
2623 if (job.name == name and
2624 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002625 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002626 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002627 raise Exception("Unable to find job %s in history" % name)
2628
2629 def assertEmptyQueues(self):
2630 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002631 for tenant in self.sched.abide.tenants.values():
2632 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002633 for pipeline_queue in pipeline.queues:
2634 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002635 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002636 pipeline.name, pipeline_queue.name,
2637 pipeline_queue.queue))
2638 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002639 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002640
2641 def assertReportedStat(self, key, value=None, kind=None):
2642 start = time.time()
2643 while time.time() < (start + 5):
2644 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002645 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002646 if key == k:
2647 if value is None and kind is None:
2648 return
2649 elif value:
2650 if value == v:
2651 return
2652 elif kind:
2653 if v.endswith('|' + kind):
2654 return
2655 time.sleep(0.1)
2656
Clark Boylanb640e052014-04-03 16:41:46 -07002657 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002658
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002659 def assertBuilds(self, builds):
2660 """Assert that the running builds are as described.
2661
2662 The list of running builds is examined and must match exactly
2663 the list of builds described by the input.
2664
2665 :arg list builds: A list of dictionaries. Each item in the
2666 list must match the corresponding build in the build
2667 history, and each element of the dictionary must match the
2668 corresponding attribute of the build.
2669
2670 """
James E. Blair3158e282016-08-19 09:34:11 -07002671 try:
2672 self.assertEqual(len(self.builds), len(builds))
2673 for i, d in enumerate(builds):
2674 for k, v in d.items():
2675 self.assertEqual(
2676 getattr(self.builds[i], k), v,
2677 "Element %i in builds does not match" % (i,))
2678 except Exception:
2679 for build in self.builds:
2680 self.log.error("Running build: %s" % build)
2681 else:
2682 self.log.error("No running builds")
2683 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002684
James E. Blairb536ecc2016-08-31 10:11:42 -07002685 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002686 """Assert that the completed builds are as described.
2687
2688 The list of completed builds is examined and must match
2689 exactly the list of builds described by the input.
2690
2691 :arg list history: A list of dictionaries. Each item in the
2692 list must match the corresponding build in the build
2693 history, and each element of the dictionary must match the
2694 corresponding attribute of the build.
2695
James E. Blairb536ecc2016-08-31 10:11:42 -07002696 :arg bool ordered: If true, the history must match the order
2697 supplied, if false, the builds are permitted to have
2698 arrived in any order.
2699
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002700 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002701 def matches(history_item, item):
2702 for k, v in item.items():
2703 if getattr(history_item, k) != v:
2704 return False
2705 return True
James E. Blair3158e282016-08-19 09:34:11 -07002706 try:
2707 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002708 if ordered:
2709 for i, d in enumerate(history):
2710 if not matches(self.history[i], d):
2711 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002712 "Element %i in history does not match %s" %
2713 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002714 else:
2715 unseen = self.history[:]
2716 for i, d in enumerate(history):
2717 found = False
2718 for unseen_item in unseen:
2719 if matches(unseen_item, d):
2720 found = True
2721 unseen.remove(unseen_item)
2722 break
2723 if not found:
2724 raise Exception("No match found for element %i "
2725 "in history" % (i,))
2726 if unseen:
2727 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002728 except Exception:
2729 for build in self.history:
2730 self.log.error("Completed build: %s" % build)
2731 else:
2732 self.log.error("No completed builds")
2733 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002734
James E. Blair6ac368c2016-12-22 18:07:20 -08002735 def printHistory(self):
2736 """Log the build history.
2737
2738 This can be useful during tests to summarize what jobs have
2739 completed.
2740
2741 """
2742 self.log.debug("Build history:")
2743 for build in self.history:
2744 self.log.debug(build)
2745
James E. Blair59fdbac2015-12-07 17:08:06 -08002746 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002747 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2748
James E. Blair9ea70072017-04-19 16:05:30 -07002749 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002750 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002751 if not os.path.exists(root):
2752 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002753 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2754 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002755- tenant:
2756 name: openstack
2757 source:
2758 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002759 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002760 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002761 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002762 - org/project
2763 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002764 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002765 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002766 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002767 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002768 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002769
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002770 def addCommitToRepo(self, project, message, files,
2771 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002772 path = os.path.join(self.upstream_root, project)
2773 repo = git.Repo(path)
2774 repo.head.reference = branch
2775 zuul.merger.merger.reset_repo_to_head(repo)
2776 for fn, content in files.items():
2777 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002778 try:
2779 os.makedirs(os.path.dirname(fn))
2780 except OSError:
2781 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002782 with open(fn, 'w') as f:
2783 f.write(content)
2784 repo.index.add([fn])
2785 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002786 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002787 repo.heads[branch].commit = commit
2788 repo.head.reference = branch
2789 repo.git.clean('-x', '-f', '-d')
2790 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002791 if tag:
2792 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002793 return before
2794
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002795 def commitConfigUpdate(self, project_name, source_name):
2796 """Commit an update to zuul.yaml
2797
2798 This overwrites the zuul.yaml in the specificed project with
2799 the contents specified.
2800
2801 :arg str project_name: The name of the project containing
2802 zuul.yaml (e.g., common-config)
2803
2804 :arg str source_name: The path to the file (underneath the
2805 test fixture directory) whose contents should be used to
2806 replace zuul.yaml.
2807 """
2808
2809 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002810 files = {}
2811 with open(source_path, 'r') as f:
2812 data = f.read()
2813 layout = yaml.safe_load(data)
2814 files['zuul.yaml'] = data
2815 for item in layout:
2816 if 'job' in item:
2817 jobname = item['job']['name']
2818 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002819 before = self.addCommitToRepo(
2820 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002821 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002822 return before
2823
Clint Byrum627ba362017-08-14 13:20:40 -07002824 def newTenantConfig(self, source_name):
2825 """ Use this to update the tenant config file in tests
2826
2827 This will update self.tenant_config_file to point to a temporary file
2828 for the duration of this particular test. The content of that file will
2829 be taken from FIXTURE_DIR/source_name
2830
2831 After the test the original value of self.tenant_config_file will be
2832 restored.
2833
2834 :arg str source_name: The path of the file under
2835 FIXTURE_DIR that will be used to populate the new tenant
2836 config file.
2837 """
2838 source_path = os.path.join(FIXTURE_DIR, source_name)
2839 orig_tenant_config_file = self.tenant_config_file
2840 with tempfile.NamedTemporaryFile(
2841 delete=False, mode='wb') as new_tenant_config:
2842 self.tenant_config_file = new_tenant_config.name
2843 with open(source_path, mode='rb') as source_tenant_config:
2844 new_tenant_config.write(source_tenant_config.read())
2845 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2846 self.setupAllProjectKeys()
2847 self.log.debug(
2848 'tenant_config_file = {}'.format(self.tenant_config_file))
2849
2850 def _restoreTenantConfig():
2851 self.log.debug(
2852 'restoring tenant_config_file = {}'.format(
2853 orig_tenant_config_file))
2854 os.unlink(self.tenant_config_file)
2855 self.tenant_config_file = orig_tenant_config_file
2856 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2857 self.addCleanup(_restoreTenantConfig)
2858
James E. Blair7fc8daa2016-08-08 15:37:15 -07002859 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002860
James E. Blair7fc8daa2016-08-08 15:37:15 -07002861 """Inject a Fake (Gerrit) event.
2862
2863 This method accepts a JSON-encoded event and simulates Zuul
2864 having received it from Gerrit. It could (and should)
2865 eventually apply to any connection type, but is currently only
2866 used with Gerrit connections. The name of the connection is
2867 used to look up the corresponding server, and the event is
2868 simulated as having been received by all Zuul connections
2869 attached to that server. So if two Gerrit connections in Zuul
2870 are connected to the same Gerrit server, and you invoke this
2871 method specifying the name of one of them, the event will be
2872 received by both.
2873
2874 .. note::
2875
2876 "self.fake_gerrit.addEvent" calls should be migrated to
2877 this method.
2878
2879 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002880 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002881 :arg str event: The JSON-encoded event.
2882
2883 """
2884 specified_conn = self.connections.connections[connection]
2885 for conn in self.connections.connections.values():
2886 if (isinstance(conn, specified_conn.__class__) and
2887 specified_conn.server == conn.server):
2888 conn.addEvent(event)
2889
James E. Blaird8af5422017-05-24 13:59:40 -07002890 def getUpstreamRepos(self, projects):
2891 """Return upstream git repo objects for the listed projects
2892
2893 :arg list projects: A list of strings, each the canonical name
2894 of a project.
2895
2896 :returns: A dictionary of {name: repo} for every listed
2897 project.
2898 :rtype: dict
2899
2900 """
2901
2902 repos = {}
2903 for project in projects:
2904 # FIXME(jeblair): the upstream root does not yet have a
2905 # hostname component; that needs to be added, and this
2906 # line removed:
2907 tmp_project_name = '/'.join(project.split('/')[1:])
2908 path = os.path.join(self.upstream_root, tmp_project_name)
2909 repo = git.Repo(path)
2910 repos[project] = repo
2911 return repos
2912
James E. Blair3f876d52016-07-22 13:07:14 -07002913
2914class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002915 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002916 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002917
Jamie Lennox7655b552017-03-17 12:33:38 +11002918 @contextmanager
2919 def jobLog(self, build):
2920 """Print job logs on assertion errors
2921
2922 This method is a context manager which, if it encounters an
2923 ecxeption, adds the build log to the debug output.
2924
2925 :arg Build build: The build that's being asserted.
2926 """
2927 try:
2928 yield
2929 except Exception:
2930 path = os.path.join(self.test_root, build.uuid,
2931 'work', 'logs', 'job-output.txt')
2932 with open(path) as f:
2933 self.log.debug(f.read())
2934 raise
2935
Joshua Heskethd78b4482015-09-14 16:56:34 -06002936
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002937class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002938 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002939 use_ssl = True
2940
2941
Joshua Heskethd78b4482015-09-14 16:56:34 -06002942class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002943 def setup_config(self):
2944 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002945 for section_name in self.config.sections():
2946 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2947 section_name, re.I)
2948 if not con_match:
2949 continue
2950
2951 if self.config.get(section_name, 'driver') == 'sql':
2952 f = MySQLSchemaFixture()
2953 self.useFixture(f)
2954 if (self.config.get(section_name, 'dburi') ==
2955 '$MYSQL_FIXTURE_DBURI$'):
2956 self.config.set(section_name, 'dburi', f.dburi)