blob: df9fbc182068583ae8d19be787f537d933ba6a5f [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,
Jesse Keating97b42482017-09-12 16:13:13 -06001015 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +02001016 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001017
Jesse Keatinga41566f2017-06-14 18:17:51 -07001018 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001019 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001020 self.pr_number += 1
1021 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001022 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001023 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001024 self.pull_requests.append(pull_request)
1025 return pull_request
1026
Jesse Keating71a47ff2017-06-06 11:36:43 -07001027 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1028 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001029 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001030 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001031 if not new_rev:
1032 new_rev = random_sha1()
1033 name = 'push'
1034 data = {
1035 'ref': ref,
1036 'before': old_rev,
1037 'after': new_rev,
1038 'repository': {
1039 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001040 },
1041 'commits': [
1042 {
1043 'added': added_files,
1044 'removed': removed_files,
1045 'modified': modified_files
1046 }
1047 ]
Wayne1a78c612015-06-11 17:14:13 -07001048 }
1049 return (name, data)
1050
Gregory Haynes4fc12542015-04-22 20:38:06 -07001051 def emitEvent(self, event):
1052 """Emulates sending the GitHub webhook event to the connection."""
1053 port = self.webapp.server.socket.getsockname()[1]
1054 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001055 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001056 secret = self.connection_config['webhook_token']
1057 signature = githubconnection._sign_request(payload, secret)
1058 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001059 req = urllib.request.Request(
1060 'http://localhost:%s/connection/%s/payload'
1061 % (port, self.connection_name),
1062 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001063 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001064
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001065 def addProject(self, project):
1066 # use the original method here and additionally register it in the
1067 # fake github
1068 super(FakeGithubConnection, self).addProject(project)
1069 self.getGithubClient(project).addProject(project)
1070
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001071 def getPull(self, project, number):
1072 pr = self.pull_requests[number - 1]
1073 data = {
1074 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001075 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001076 'updated_at': pr.updated_at,
1077 'base': {
1078 'repo': {
1079 'full_name': pr.project
1080 },
1081 'ref': pr.branch,
1082 },
Jan Hruban37615e52015-11-19 14:30:49 +01001083 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001084 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001085 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001086 'sha': pr.head_sha,
1087 'repo': {
1088 'full_name': pr.project
1089 }
Jesse Keating61040e72017-06-08 15:08:27 -07001090 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001091 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001092 'labels': pr.labels,
1093 'merged': pr.is_merged,
1094 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001095 }
1096 return data
1097
Jesse Keating9021a012017-08-29 14:45:27 -07001098 def getPullBySha(self, sha, project):
1099 prs = list(set([p for p in self.pull_requests if
1100 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001101 if len(prs) > 1:
1102 raise Exception('Multiple pulls found with head sha: %s' % sha)
1103 pr = prs[0]
1104 return self.getPull(pr.project, pr.number)
1105
Jesse Keatingae4cd272017-01-30 17:10:44 -08001106 def _getPullReviews(self, owner, project, number):
1107 pr = self.pull_requests[number - 1]
1108 return pr.reviews
1109
Jesse Keatingae4cd272017-01-30 17:10:44 -08001110 def getRepoPermission(self, project, login):
1111 owner, proj = project.split('/')
1112 for pr in self.pull_requests:
1113 pr_owner, pr_project = pr.project.split('/')
1114 if (pr_owner == owner and proj == pr_project):
1115 if login in pr.writers:
1116 return 'write'
1117 else:
1118 return 'read'
1119
Gregory Haynes4fc12542015-04-22 20:38:06 -07001120 def getGitUrl(self, project):
1121 return os.path.join(self.upstream_root, str(project))
1122
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001123 def real_getGitUrl(self, project):
1124 return super(FakeGithubConnection, self).getGitUrl(project)
1125
Jan Hrubane252a732017-01-03 15:03:09 +01001126 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001127 # record that this got reported
1128 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001129 pull_request = self.pull_requests[pr_number - 1]
1130 pull_request.addComment(message)
1131
Jan Hruban3b415922016-02-03 13:10:22 +01001132 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001133 # record that this got reported
1134 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001135 pull_request = self.pull_requests[pr_number - 1]
1136 if self.merge_failure:
1137 raise Exception('Pull request was not merged')
1138 if self.merge_not_allowed_count > 0:
1139 self.merge_not_allowed_count -= 1
1140 raise MergeFailure('Merge was not successful due to mergeability'
1141 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001142 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001143
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001144 def setCommitStatus(self, project, sha, state, url='', description='',
1145 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001146 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001147 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001148 super(FakeGithubConnection, self).setCommitStatus(
1149 project, sha, state,
1150 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001151
Jan Hruban16ad31f2015-11-07 14:39:07 +01001152 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001153 # record that this got reported
1154 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001155 pull_request = self.pull_requests[pr_number - 1]
1156 pull_request.addLabel(label)
1157
1158 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001159 # record that this got reported
1160 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001161 pull_request = self.pull_requests[pr_number - 1]
1162 pull_request.removeLabel(label)
1163
Jesse Keatinga41566f2017-06-14 18:17:51 -07001164 def _getNeededByFromPR(self, change):
1165 prs = []
1166 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001167 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001168 change.number))
1169 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001170 if not pr.body:
1171 body = ''
1172 else:
1173 body = pr.body
1174 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001175 # Get our version of a pull so that it's a dict
1176 pull = self.getPull(pr.project, pr.number)
1177 prs.append(pull)
1178
1179 return prs
1180
Gregory Haynes4fc12542015-04-22 20:38:06 -07001181
Clark Boylanb640e052014-04-03 16:41:46 -07001182class BuildHistory(object):
1183 def __init__(self, **kw):
1184 self.__dict__.update(kw)
1185
1186 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001187 return ("<Completed build, result: %s name: %s uuid: %s "
1188 "changes: %s ref: %s>" %
1189 (self.result, self.name, self.uuid,
1190 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001191
1192
Clark Boylanb640e052014-04-03 16:41:46 -07001193class FakeStatsd(threading.Thread):
1194 def __init__(self):
1195 threading.Thread.__init__(self)
1196 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001197 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001198 self.sock.bind(('', 0))
1199 self.port = self.sock.getsockname()[1]
1200 self.wake_read, self.wake_write = os.pipe()
1201 self.stats = []
1202
1203 def run(self):
1204 while True:
1205 poll = select.poll()
1206 poll.register(self.sock, select.POLLIN)
1207 poll.register(self.wake_read, select.POLLIN)
1208 ret = poll.poll()
1209 for (fd, event) in ret:
1210 if fd == self.sock.fileno():
1211 data = self.sock.recvfrom(1024)
1212 if not data:
1213 return
1214 self.stats.append(data[0])
1215 if fd == self.wake_read:
1216 return
1217
1218 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001219 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001220
1221
James E. Blaire1767bc2016-08-02 10:00:27 -07001222class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001223 log = logging.getLogger("zuul.test")
1224
Paul Belanger174a8272017-03-14 13:20:10 -04001225 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001226 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001227 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001228 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001229 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001230 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001231 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001232 # TODOv3(jeblair): self.node is really "the label of the node
1233 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001234 # keep using it like this, or we may end up exposing more of
1235 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001236 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001237 self.node = None
1238 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001239 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001240 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001241 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001242 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001243 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001244 self.wait_condition = threading.Condition()
1245 self.waiting = False
1246 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001247 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001248 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001249 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001250 items = self.parameters['zuul']['items']
1251 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1252 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001253
James E. Blair3158e282016-08-19 09:34:11 -07001254 def __repr__(self):
1255 waiting = ''
1256 if self.waiting:
1257 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001258 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1259 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001260
Clark Boylanb640e052014-04-03 16:41:46 -07001261 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001262 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001263 self.wait_condition.acquire()
1264 self.wait_condition.notify()
1265 self.waiting = False
1266 self.log.debug("Build %s released" % self.unique)
1267 self.wait_condition.release()
1268
1269 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001270 """Return whether this build is being held.
1271
1272 :returns: Whether the build is being held.
1273 :rtype: bool
1274 """
1275
Clark Boylanb640e052014-04-03 16:41:46 -07001276 self.wait_condition.acquire()
1277 if self.waiting:
1278 ret = True
1279 else:
1280 ret = False
1281 self.wait_condition.release()
1282 return ret
1283
1284 def _wait(self):
1285 self.wait_condition.acquire()
1286 self.waiting = True
1287 self.log.debug("Build %s waiting" % self.unique)
1288 self.wait_condition.wait()
1289 self.wait_condition.release()
1290
1291 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001292 self.log.debug('Running build %s' % self.unique)
1293
Paul Belanger174a8272017-03-14 13:20:10 -04001294 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001295 self.log.debug('Holding build %s' % self.unique)
1296 self._wait()
1297 self.log.debug("Build %s continuing" % self.unique)
1298
James E. Blair412fba82017-01-26 15:00:50 -08001299 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001300 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001301 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001302 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001303 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001304 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001305 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001306
James E. Blaire1767bc2016-08-02 10:00:27 -07001307 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001308
James E. Blaira5dba232016-08-08 15:53:24 -07001309 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001310 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001311 for change in changes:
1312 if self.hasChanges(change):
1313 return True
1314 return False
1315
James E. Blaire7b99a02016-08-05 14:27:34 -07001316 def hasChanges(self, *changes):
1317 """Return whether this build has certain changes in its git repos.
1318
1319 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001320 are expected to be present (in order) in the git repository of
1321 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001322
1323 :returns: Whether the build has the indicated changes.
1324 :rtype: bool
1325
1326 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001327 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001328 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001329 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001330 try:
1331 repo = git.Repo(path)
1332 except NoSuchPathError as e:
1333 self.log.debug('%s' % e)
1334 return False
James E. Blair247cab72017-07-20 16:52:36 -07001335 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001336 commit_message = '%s-1' % change.subject
1337 self.log.debug("Checking if build %s has changes; commit_message "
1338 "%s; repo_messages %s" % (self, commit_message,
1339 repo_messages))
1340 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001341 self.log.debug(" messages do not match")
1342 return False
1343 self.log.debug(" OK")
1344 return True
1345
James E. Blaird8af5422017-05-24 13:59:40 -07001346 def getWorkspaceRepos(self, projects):
1347 """Return workspace git repo objects for the listed projects
1348
1349 :arg list projects: A list of strings, each the canonical name
1350 of a project.
1351
1352 :returns: A dictionary of {name: repo} for every listed
1353 project.
1354 :rtype: dict
1355
1356 """
1357
1358 repos = {}
1359 for project in projects:
1360 path = os.path.join(self.jobdir.src_root, project)
1361 repo = git.Repo(path)
1362 repos[project] = repo
1363 return repos
1364
Clark Boylanb640e052014-04-03 16:41:46 -07001365
Paul Belanger174a8272017-03-14 13:20:10 -04001366class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1367 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001368
Paul Belanger174a8272017-03-14 13:20:10 -04001369 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001370 they will report that they have started but then pause until
1371 released before reporting completion. This attribute may be
1372 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001373 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001374 be explicitly released.
1375
1376 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001377 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001378 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001379 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001380 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001381 self.hold_jobs_in_build = False
1382 self.lock = threading.Lock()
1383 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001384 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001385 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001386 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001387
James E. Blaira5dba232016-08-08 15:53:24 -07001388 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001389 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001390
1391 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001392 :arg Change change: The :py:class:`~tests.base.FakeChange`
1393 instance which should cause the job to fail. This job
1394 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001395
1396 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 l = self.fail_tests.get(name, [])
1398 l.append(change)
1399 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001400
James E. Blair962220f2016-08-03 11:22:38 -07001401 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001402 """Release a held build.
1403
1404 :arg str regex: A regular expression which, if supplied, will
1405 cause only builds with matching names to be released. If
1406 not supplied, all builds will be released.
1407
1408 """
James E. Blair962220f2016-08-03 11:22:38 -07001409 builds = self.running_builds[:]
1410 self.log.debug("Releasing build %s (%s)" % (regex,
1411 len(self.running_builds)))
1412 for build in builds:
1413 if not regex or re.match(regex, build.name):
1414 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001415 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001416 build.release()
1417 else:
1418 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001419 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001420 self.log.debug("Done releasing builds %s (%s)" %
1421 (regex, len(self.running_builds)))
1422
Paul Belanger174a8272017-03-14 13:20:10 -04001423 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001424 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001425 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001426 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001427 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001428 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001429 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001430 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001431 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1432 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001433
1434 def stopJob(self, job):
1435 self.log.debug("handle stop")
1436 parameters = json.loads(job.arguments)
1437 uuid = parameters['uuid']
1438 for build in self.running_builds:
1439 if build.unique == uuid:
1440 build.aborted = True
1441 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001442 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001443
James E. Blaira002b032017-04-18 10:35:48 -07001444 def stop(self):
1445 for build in self.running_builds:
1446 build.release()
1447 super(RecordingExecutorServer, self).stop()
1448
Joshua Hesketh50c21782016-10-13 21:34:14 +11001449
Paul Belanger174a8272017-03-14 13:20:10 -04001450class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001451 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001452 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001453 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001454 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001455 if not commit: # merge conflict
1456 self.recordResult('MERGER_FAILURE')
1457 return commit
1458
1459 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001460 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001461 self.executor_server.lock.acquire()
1462 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001463 BuildHistory(name=build.name, result=result, changes=build.changes,
1464 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001465 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001466 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001467 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001468 )
Paul Belanger174a8272017-03-14 13:20:10 -04001469 self.executor_server.running_builds.remove(build)
1470 del self.executor_server.job_builds[self.job.unique]
1471 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001472
1473 def runPlaybooks(self, args):
1474 build = self.executor_server.job_builds[self.job.unique]
1475 build.jobdir = self.jobdir
1476
1477 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1478 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001479 return result
1480
James E. Blair892cca62017-08-09 11:36:58 -07001481 def runAnsible(self, cmd, timeout, playbook):
Paul Belanger174a8272017-03-14 13:20:10 -04001482 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001483
Paul Belanger174a8272017-03-14 13:20:10 -04001484 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001485 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair892cca62017-08-09 11:36:58 -07001486 cmd, timeout, playbook)
James E. Blair412fba82017-01-26 15:00:50 -08001487 else:
1488 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001489 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001490
James E. Blairad8dca02017-02-21 11:48:32 -05001491 def getHostList(self, args):
1492 self.log.debug("hostlist")
1493 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001494 for host in hosts:
1495 host['host_vars']['ansible_connection'] = 'local'
1496
1497 hosts.append(dict(
1498 name='localhost',
1499 host_vars=dict(ansible_connection='local'),
1500 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001501 return hosts
1502
James E. Blairf5dbd002015-12-23 15:26:17 -08001503
Clark Boylanb640e052014-04-03 16:41:46 -07001504class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001505 """A Gearman server for use in tests.
1506
1507 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1508 added to the queue but will not be distributed to workers
1509 until released. This attribute may be changed at any time and
1510 will take effect for subsequently enqueued jobs, but
1511 previously held jobs will still need to be explicitly
1512 released.
1513
1514 """
1515
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001516 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001517 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001518 if use_ssl:
1519 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1520 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1521 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1522 else:
1523 ssl_ca = None
1524 ssl_cert = None
1525 ssl_key = None
1526
1527 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1528 ssl_cert=ssl_cert,
1529 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001530
1531 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001532 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1533 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001534 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001535 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001536 job.waiting = self.hold_jobs_in_queue
1537 else:
1538 job.waiting = False
1539 if job.waiting:
1540 continue
1541 if job.name in connection.functions:
1542 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001543 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001544 connection.related_jobs[job.handle] = job
1545 job.worker_connection = connection
1546 job.running = True
1547 return job
1548 return None
1549
1550 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001551 """Release a held job.
1552
1553 :arg str regex: A regular expression which, if supplied, will
1554 cause only jobs with matching names to be released. If
1555 not supplied, all jobs will be released.
1556 """
Clark Boylanb640e052014-04-03 16:41:46 -07001557 released = False
1558 qlen = (len(self.high_queue) + len(self.normal_queue) +
1559 len(self.low_queue))
1560 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1561 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001562 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001563 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001564 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001565 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001566 self.log.debug("releasing queued job %s" %
1567 job.unique)
1568 job.waiting = False
1569 released = True
1570 else:
1571 self.log.debug("not releasing queued job %s" %
1572 job.unique)
1573 if released:
1574 self.wakeConnections()
1575 qlen = (len(self.high_queue) + len(self.normal_queue) +
1576 len(self.low_queue))
1577 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1578
1579
1580class FakeSMTP(object):
1581 log = logging.getLogger('zuul.FakeSMTP')
1582
1583 def __init__(self, messages, server, port):
1584 self.server = server
1585 self.port = port
1586 self.messages = messages
1587
1588 def sendmail(self, from_email, to_email, msg):
1589 self.log.info("Sending email from %s, to %s, with msg %s" % (
1590 from_email, to_email, msg))
1591
1592 headers = msg.split('\n\n', 1)[0]
1593 body = msg.split('\n\n', 1)[1]
1594
1595 self.messages.append(dict(
1596 from_email=from_email,
1597 to_email=to_email,
1598 msg=msg,
1599 headers=headers,
1600 body=body,
1601 ))
1602
1603 return True
1604
1605 def quit(self):
1606 return True
1607
1608
James E. Blairdce6cea2016-12-20 16:45:32 -08001609class FakeNodepool(object):
1610 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001611 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001612
1613 log = logging.getLogger("zuul.test.FakeNodepool")
1614
1615 def __init__(self, host, port, chroot):
1616 self.client = kazoo.client.KazooClient(
1617 hosts='%s:%s%s' % (host, port, chroot))
1618 self.client.start()
1619 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001620 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001621 self.thread = threading.Thread(target=self.run)
1622 self.thread.daemon = True
1623 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001624 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001625
1626 def stop(self):
1627 self._running = False
1628 self.thread.join()
1629 self.client.stop()
1630 self.client.close()
1631
1632 def run(self):
1633 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001634 try:
1635 self._run()
1636 except Exception:
1637 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001638 time.sleep(0.1)
1639
1640 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001641 if self.paused:
1642 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001643 for req in self.getNodeRequests():
1644 self.fulfillRequest(req)
1645
1646 def getNodeRequests(self):
1647 try:
1648 reqids = self.client.get_children(self.REQUEST_ROOT)
1649 except kazoo.exceptions.NoNodeError:
1650 return []
1651 reqs = []
1652 for oid in sorted(reqids):
1653 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001654 try:
1655 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001656 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001657 data['_oid'] = oid
1658 reqs.append(data)
1659 except kazoo.exceptions.NoNodeError:
1660 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001661 return reqs
1662
James E. Blaire18d4602017-01-05 11:17:28 -08001663 def getNodes(self):
1664 try:
1665 nodeids = self.client.get_children(self.NODE_ROOT)
1666 except kazoo.exceptions.NoNodeError:
1667 return []
1668 nodes = []
1669 for oid in sorted(nodeids):
1670 path = self.NODE_ROOT + '/' + oid
1671 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001672 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001673 data['_oid'] = oid
1674 try:
1675 lockfiles = self.client.get_children(path + '/lock')
1676 except kazoo.exceptions.NoNodeError:
1677 lockfiles = []
1678 if lockfiles:
1679 data['_lock'] = True
1680 else:
1681 data['_lock'] = False
1682 nodes.append(data)
1683 return nodes
1684
James E. Blaira38c28e2017-01-04 10:33:20 -08001685 def makeNode(self, request_id, node_type):
1686 now = time.time()
1687 path = '/nodepool/nodes/'
1688 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001689 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001690 provider='test-provider',
1691 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001692 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001693 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001694 public_ipv4='127.0.0.1',
1695 private_ipv4=None,
1696 public_ipv6=None,
1697 allocated_to=request_id,
1698 state='ready',
1699 state_time=now,
1700 created_time=now,
1701 updated_time=now,
1702 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001703 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001704 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001705 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001706 path = self.client.create(path, data,
1707 makepath=True,
1708 sequence=True)
1709 nodeid = path.split("/")[-1]
1710 return nodeid
1711
James E. Blair6ab79e02017-01-06 10:10:17 -08001712 def addFailRequest(self, request):
1713 self.fail_requests.add(request['_oid'])
1714
James E. Blairdce6cea2016-12-20 16:45:32 -08001715 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001716 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001717 return
1718 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001719 oid = request['_oid']
1720 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001721
James E. Blair6ab79e02017-01-06 10:10:17 -08001722 if oid in self.fail_requests:
1723 request['state'] = 'failed'
1724 else:
1725 request['state'] = 'fulfilled'
1726 nodes = []
1727 for node in request['node_types']:
1728 nodeid = self.makeNode(oid, node)
1729 nodes.append(nodeid)
1730 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001731
James E. Blaira38c28e2017-01-04 10:33:20 -08001732 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001733 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001734 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001735 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001736 try:
1737 self.client.set(path, data)
1738 except kazoo.exceptions.NoNodeError:
1739 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001740
1741
James E. Blair498059b2016-12-20 13:50:13 -08001742class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001743 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001744 super(ChrootedKazooFixture, self).__init__()
1745
1746 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1747 if ':' in zk_host:
1748 host, port = zk_host.split(':')
1749 else:
1750 host = zk_host
1751 port = None
1752
1753 self.zookeeper_host = host
1754
1755 if not port:
1756 self.zookeeper_port = 2181
1757 else:
1758 self.zookeeper_port = int(port)
1759
Clark Boylan621ec9a2017-04-07 17:41:33 -07001760 self.test_id = test_id
1761
James E. Blair498059b2016-12-20 13:50:13 -08001762 def _setUp(self):
1763 # Make sure the test chroot paths do not conflict
1764 random_bits = ''.join(random.choice(string.ascii_lowercase +
1765 string.ascii_uppercase)
1766 for x in range(8))
1767
Clark Boylan621ec9a2017-04-07 17:41:33 -07001768 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001769 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1770
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001771 self.addCleanup(self._cleanup)
1772
James E. Blair498059b2016-12-20 13:50:13 -08001773 # Ensure the chroot path exists and clean up any pre-existing znodes.
1774 _tmp_client = kazoo.client.KazooClient(
1775 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1776 _tmp_client.start()
1777
1778 if _tmp_client.exists(self.zookeeper_chroot):
1779 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1780
1781 _tmp_client.ensure_path(self.zookeeper_chroot)
1782 _tmp_client.stop()
1783 _tmp_client.close()
1784
James E. Blair498059b2016-12-20 13:50:13 -08001785 def _cleanup(self):
1786 '''Remove the chroot path.'''
1787 # Need a non-chroot'ed client to remove the chroot path
1788 _tmp_client = kazoo.client.KazooClient(
1789 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1790 _tmp_client.start()
1791 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1792 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001793 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001794
1795
Joshua Heskethd78b4482015-09-14 16:56:34 -06001796class MySQLSchemaFixture(fixtures.Fixture):
1797 def setUp(self):
1798 super(MySQLSchemaFixture, self).setUp()
1799
1800 random_bits = ''.join(random.choice(string.ascii_lowercase +
1801 string.ascii_uppercase)
1802 for x in range(8))
1803 self.name = '%s_%s' % (random_bits, os.getpid())
1804 self.passwd = uuid.uuid4().hex
1805 db = pymysql.connect(host="localhost",
1806 user="openstack_citest",
1807 passwd="openstack_citest",
1808 db="openstack_citest")
1809 cur = db.cursor()
1810 cur.execute("create database %s" % self.name)
1811 cur.execute(
1812 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1813 (self.name, self.name, self.passwd))
1814 cur.execute("flush privileges")
1815
1816 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1817 self.passwd,
1818 self.name)
1819 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1820 self.addCleanup(self.cleanup)
1821
1822 def cleanup(self):
1823 db = pymysql.connect(host="localhost",
1824 user="openstack_citest",
1825 passwd="openstack_citest",
1826 db="openstack_citest")
1827 cur = db.cursor()
1828 cur.execute("drop database %s" % self.name)
1829 cur.execute("drop user '%s'@'localhost'" % self.name)
1830 cur.execute("flush privileges")
1831
1832
Maru Newby3fe5f852015-01-13 04:22:14 +00001833class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001834 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001835 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001836
James E. Blair1c236df2017-02-01 14:07:24 -08001837 def attachLogs(self, *args):
1838 def reader():
1839 self._log_stream.seek(0)
1840 while True:
1841 x = self._log_stream.read(4096)
1842 if not x:
1843 break
1844 yield x.encode('utf8')
1845 content = testtools.content.content_from_reader(
1846 reader,
1847 testtools.content_type.UTF8_TEXT,
1848 False)
1849 self.addDetail('logging', content)
1850
Clark Boylanb640e052014-04-03 16:41:46 -07001851 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001852 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001853 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1854 try:
1855 test_timeout = int(test_timeout)
1856 except ValueError:
1857 # If timeout value is invalid do not set a timeout.
1858 test_timeout = 0
1859 if test_timeout > 0:
1860 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1861
1862 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1863 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1864 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1865 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1866 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1867 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1868 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1869 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1870 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1871 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001872 self._log_stream = StringIO()
1873 self.addOnException(self.attachLogs)
1874 else:
1875 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001876
James E. Blair73b41772017-05-22 13:22:55 -07001877 # NOTE(jeblair): this is temporary extra debugging to try to
1878 # track down a possible leak.
1879 orig_git_repo_init = git.Repo.__init__
1880
1881 def git_repo_init(myself, *args, **kw):
1882 orig_git_repo_init(myself, *args, **kw)
1883 self.log.debug("Created git repo 0x%x %s" %
1884 (id(myself), repr(myself)))
1885
1886 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1887 git_repo_init))
1888
James E. Blair1c236df2017-02-01 14:07:24 -08001889 handler = logging.StreamHandler(self._log_stream)
1890 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1891 '%(levelname)-8s %(message)s')
1892 handler.setFormatter(formatter)
1893
1894 logger = logging.getLogger()
1895 logger.setLevel(logging.DEBUG)
1896 logger.addHandler(handler)
1897
Clark Boylan3410d532017-04-25 12:35:29 -07001898 # Make sure we don't carry old handlers around in process state
1899 # which slows down test runs
1900 self.addCleanup(logger.removeHandler, handler)
1901 self.addCleanup(handler.close)
1902 self.addCleanup(handler.flush)
1903
James E. Blair1c236df2017-02-01 14:07:24 -08001904 # NOTE(notmorgan): Extract logging overrides for specific
1905 # libraries from the OS_LOG_DEFAULTS env and create loggers
1906 # for each. This is used to limit the output during test runs
1907 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001908 log_defaults_from_env = os.environ.get(
1909 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001910 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001911
James E. Blairdce6cea2016-12-20 16:45:32 -08001912 if log_defaults_from_env:
1913 for default in log_defaults_from_env.split(','):
1914 try:
1915 name, level_str = default.split('=', 1)
1916 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001917 logger = logging.getLogger(name)
1918 logger.setLevel(level)
1919 logger.addHandler(handler)
1920 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001921 except ValueError:
1922 # NOTE(notmorgan): Invalid format of the log default,
1923 # skip and don't try and apply a logger for the
1924 # specified module
1925 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001926
Maru Newby3fe5f852015-01-13 04:22:14 +00001927
1928class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001929 """A test case with a functioning Zuul.
1930
1931 The following class variables are used during test setup and can
1932 be overidden by subclasses but are effectively read-only once a
1933 test method starts running:
1934
1935 :cvar str config_file: This points to the main zuul config file
1936 within the fixtures directory. Subclasses may override this
1937 to obtain a different behavior.
1938
1939 :cvar str tenant_config_file: This is the tenant config file
1940 (which specifies from what git repos the configuration should
1941 be loaded). It defaults to the value specified in
1942 `config_file` but can be overidden by subclasses to obtain a
1943 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001944 configuration. See also the :py:func:`simple_layout`
1945 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001946
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001947 :cvar bool create_project_keys: Indicates whether Zuul should
1948 auto-generate keys for each project, or whether the test
1949 infrastructure should insert dummy keys to save time during
1950 startup. Defaults to False.
1951
James E. Blaire7b99a02016-08-05 14:27:34 -07001952 The following are instance variables that are useful within test
1953 methods:
1954
1955 :ivar FakeGerritConnection fake_<connection>:
1956 A :py:class:`~tests.base.FakeGerritConnection` will be
1957 instantiated for each connection present in the config file
1958 and stored here. For instance, `fake_gerrit` will hold the
1959 FakeGerritConnection object for a connection named `gerrit`.
1960
1961 :ivar FakeGearmanServer gearman_server: An instance of
1962 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1963 server that all of the Zuul components in this test use to
1964 communicate with each other.
1965
Paul Belanger174a8272017-03-14 13:20:10 -04001966 :ivar RecordingExecutorServer executor_server: An instance of
1967 :py:class:`~tests.base.RecordingExecutorServer` which is the
1968 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001969
1970 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1971 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001972 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001973 list upon completion.
1974
1975 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1976 objects representing completed builds. They are appended to
1977 the list in the order they complete.
1978
1979 """
1980
James E. Blair83005782015-12-11 14:46:03 -08001981 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001982 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001983 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001984 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001985
1986 def _startMerger(self):
1987 self.merge_server = zuul.merger.server.MergeServer(self.config,
1988 self.connections)
1989 self.merge_server.start()
1990
Maru Newby3fe5f852015-01-13 04:22:14 +00001991 def setUp(self):
1992 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001993
1994 self.setupZK()
1995
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001996 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001997 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001998 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1999 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002000 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002001 tmp_root = tempfile.mkdtemp(
2002 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002003 self.test_root = os.path.join(tmp_root, "zuul-test")
2004 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002005 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002006 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002007 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002008 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2009 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002010
2011 if os.path.exists(self.test_root):
2012 shutil.rmtree(self.test_root)
2013 os.makedirs(self.test_root)
2014 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002015 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002016 os.makedirs(self.merger_state_root)
2017 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002018
2019 # Make per test copy of Configuration.
2020 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002021 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2022 if not os.path.exists(self.private_key_file):
2023 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2024 shutil.copy(src_private_key_file, self.private_key_file)
2025 shutil.copy('{}.pub'.format(src_private_key_file),
2026 '{}.pub'.format(self.private_key_file))
2027 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002028 self.config.set('scheduler', 'tenant_config',
2029 os.path.join(
2030 FIXTURE_DIR,
2031 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002032 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002033 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002034 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002035 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002036 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002037
Clark Boylanb640e052014-04-03 16:41:46 -07002038 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10002039 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
2040 # see: https://github.com/jsocol/pystatsd/issues/61
2041 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07002042 os.environ['STATSD_PORT'] = str(self.statsd.port)
2043 self.statsd.start()
2044 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05002045 importlib.reload(statsd)
2046 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07002047
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002048 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002049
2050 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002051 self.log.info("Gearman server on port %s" %
2052 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002053 if self.use_ssl:
2054 self.log.info('SSL enabled for gearman')
2055 self.config.set(
2056 'gearman', 'ssl_ca',
2057 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2058 self.config.set(
2059 'gearman', 'ssl_cert',
2060 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2061 self.config.set(
2062 'gearman', 'ssl_key',
2063 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002064
James E. Blaire511d2f2016-12-08 15:22:26 -08002065 gerritsource.GerritSource.replication_timeout = 1.5
2066 gerritsource.GerritSource.replication_retry_interval = 0.5
2067 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002068
Joshua Hesketh352264b2015-08-11 23:42:08 +10002069 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07002070
Jan Hruban7083edd2015-08-21 14:00:54 +02002071 self.webapp = zuul.webapp.WebApp(
2072 self.sched, port=0, listen_address='127.0.0.1')
2073
Jan Hruban6b71aff2015-10-22 16:58:08 +02002074 self.event_queues = [
2075 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002076 self.sched.trigger_event_queue,
2077 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002078 ]
2079
James E. Blairfef78942016-03-11 16:28:56 -08002080 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002081 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002082
Paul Belanger174a8272017-03-14 13:20:10 -04002083 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002084 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002085 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002086 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002087 _test_root=self.test_root,
2088 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002089 self.executor_server.start()
2090 self.history = self.executor_server.build_history
2091 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002092
Paul Belanger174a8272017-03-14 13:20:10 -04002093 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002094 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002095 self.merge_client = zuul.merger.client.MergeClient(
2096 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002097 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002098 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002099 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002100
James E. Blair0d5a36e2017-02-21 10:53:44 -05002101 self.fake_nodepool = FakeNodepool(
2102 self.zk_chroot_fixture.zookeeper_host,
2103 self.zk_chroot_fixture.zookeeper_port,
2104 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002105
Paul Belanger174a8272017-03-14 13:20:10 -04002106 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002107 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002108 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002109 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002110
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002111 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002112
2113 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002114 self.webapp.start()
2115 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002116 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002117 # Cleanups are run in reverse order
2118 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002119 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002120 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002121
James E. Blairb9c0d772017-03-03 14:34:49 -08002122 self.sched.reconfigure(self.config)
2123 self.sched.resume()
2124
Tobias Henkel7df274b2017-05-26 17:41:11 +02002125 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002126 # Set up gerrit related fakes
2127 # Set a changes database so multiple FakeGerrit's can report back to
2128 # a virtual canonical database given by the configured hostname
2129 self.gerrit_changes_dbs = {}
2130
2131 def getGerritConnection(driver, name, config):
2132 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2133 con = FakeGerritConnection(driver, name, config,
2134 changes_db=db,
2135 upstream_root=self.upstream_root)
2136 self.event_queues.append(con.event_queue)
2137 setattr(self, 'fake_' + name, con)
2138 return con
2139
2140 self.useFixture(fixtures.MonkeyPatch(
2141 'zuul.driver.gerrit.GerritDriver.getConnection',
2142 getGerritConnection))
2143
Gregory Haynes4fc12542015-04-22 20:38:06 -07002144 def getGithubConnection(driver, name, config):
2145 con = FakeGithubConnection(driver, name, config,
2146 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002147 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002148 setattr(self, 'fake_' + name, con)
2149 return con
2150
2151 self.useFixture(fixtures.MonkeyPatch(
2152 'zuul.driver.github.GithubDriver.getConnection',
2153 getGithubConnection))
2154
James E. Blaire511d2f2016-12-08 15:22:26 -08002155 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002156 # TODO(jhesketh): This should come from lib.connections for better
2157 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002158 # Register connections from the config
2159 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002160
Joshua Hesketh352264b2015-08-11 23:42:08 +10002161 def FakeSMTPFactory(*args, **kw):
2162 args = [self.smtp_messages] + list(args)
2163 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002164
Joshua Hesketh352264b2015-08-11 23:42:08 +10002165 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002166
James E. Blaire511d2f2016-12-08 15:22:26 -08002167 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002168 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002169 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002170
James E. Blair83005782015-12-11 14:46:03 -08002171 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002172 # This creates the per-test configuration object. It can be
2173 # overriden by subclasses, but should not need to be since it
2174 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002175 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002176 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002177
James E. Blair39840362017-06-23 20:34:02 +01002178 sections = ['zuul', 'scheduler', 'executor', 'merger']
2179 for section in sections:
2180 if not self.config.has_section(section):
2181 self.config.add_section(section)
2182
James E. Blair06cc3922017-04-19 10:08:10 -07002183 if not self.setupSimpleLayout():
2184 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002185 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002186 self.tenant_config_file)
2187 git_path = os.path.join(
2188 os.path.dirname(
2189 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2190 'git')
2191 if os.path.exists(git_path):
2192 for reponame in os.listdir(git_path):
2193 project = reponame.replace('_', '/')
2194 self.copyDirToRepo(project,
2195 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002196 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002197 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002198 self.setupAllProjectKeys()
2199
James E. Blair06cc3922017-04-19 10:08:10 -07002200 def setupSimpleLayout(self):
2201 # If the test method has been decorated with a simple_layout,
2202 # use that instead of the class tenant_config_file. Set up a
2203 # single config-project with the specified layout, and
2204 # initialize repos for all of the 'project' entries which
2205 # appear in the layout.
2206 test_name = self.id().split('.')[-1]
2207 test = getattr(self, test_name)
2208 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002209 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002210 else:
2211 return False
2212
James E. Blairb70e55a2017-04-19 12:57:02 -07002213 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002214 path = os.path.join(FIXTURE_DIR, path)
2215 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002216 data = f.read()
2217 layout = yaml.safe_load(data)
2218 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002219 untrusted_projects = []
2220 for item in layout:
2221 if 'project' in item:
2222 name = item['project']['name']
2223 untrusted_projects.append(name)
2224 self.init_repo(name)
2225 self.addCommitToRepo(name, 'initial commit',
2226 files={'README': ''},
2227 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002228 if 'job' in item:
2229 jobname = item['job']['name']
2230 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002231
2232 root = os.path.join(self.test_root, "config")
2233 if not os.path.exists(root):
2234 os.makedirs(root)
2235 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2236 config = [{'tenant':
2237 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002238 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002239 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002240 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002241 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002242 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002243 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002244 os.path.join(FIXTURE_DIR, f.name))
2245
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002246 self.init_repo('org/common-config')
2247 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002248 files, branch='master', tag='init')
2249
2250 return True
2251
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002252 def setupAllProjectKeys(self):
2253 if self.create_project_keys:
2254 return
2255
James E. Blair39840362017-06-23 20:34:02 +01002256 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002257 with open(os.path.join(FIXTURE_DIR, path)) as f:
2258 tenant_config = yaml.safe_load(f.read())
2259 for tenant in tenant_config:
2260 sources = tenant['tenant']['source']
2261 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002262 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002263 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002264 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002265 self.setupProjectKeys(source, project)
2266
2267 def setupProjectKeys(self, source, project):
2268 # Make sure we set up an RSA key for the project so that we
2269 # don't spend time generating one:
2270
James E. Blair6459db12017-06-29 14:57:20 -07002271 if isinstance(project, dict):
2272 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002273 key_root = os.path.join(self.state_root, 'keys')
2274 if not os.path.isdir(key_root):
2275 os.mkdir(key_root, 0o700)
2276 private_key_file = os.path.join(key_root, source, project + '.pem')
2277 private_key_dir = os.path.dirname(private_key_file)
2278 self.log.debug("Installing test keys for project %s at %s" % (
2279 project, private_key_file))
2280 if not os.path.isdir(private_key_dir):
2281 os.makedirs(private_key_dir)
2282 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2283 with open(private_key_file, 'w') as o:
2284 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002285
James E. Blair498059b2016-12-20 13:50:13 -08002286 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002287 self.zk_chroot_fixture = self.useFixture(
2288 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002289 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002290 self.zk_chroot_fixture.zookeeper_host,
2291 self.zk_chroot_fixture.zookeeper_port,
2292 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002293
James E. Blair96c6bf82016-01-15 16:20:40 -08002294 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002295 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002296
2297 files = {}
2298 for (dirpath, dirnames, filenames) in os.walk(source_path):
2299 for filename in filenames:
2300 test_tree_filepath = os.path.join(dirpath, filename)
2301 common_path = os.path.commonprefix([test_tree_filepath,
2302 source_path])
2303 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2304 with open(test_tree_filepath, 'r') as f:
2305 content = f.read()
2306 files[relative_filepath] = content
2307 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002308 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002309
James E. Blaire18d4602017-01-05 11:17:28 -08002310 def assertNodepoolState(self):
2311 # Make sure that there are no pending requests
2312
2313 requests = self.fake_nodepool.getNodeRequests()
2314 self.assertEqual(len(requests), 0)
2315
2316 nodes = self.fake_nodepool.getNodes()
2317 for node in nodes:
2318 self.assertFalse(node['_lock'], "Node %s is locked" %
2319 (node['_oid'],))
2320
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002321 def assertNoGeneratedKeys(self):
2322 # Make sure that Zuul did not generate any project keys
2323 # (unless it was supposed to).
2324
2325 if self.create_project_keys:
2326 return
2327
2328 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2329 test_key = i.read()
2330
2331 key_root = os.path.join(self.state_root, 'keys')
2332 for root, dirname, files in os.walk(key_root):
2333 for fn in files:
2334 with open(os.path.join(root, fn)) as f:
2335 self.assertEqual(test_key, f.read())
2336
Clark Boylanb640e052014-04-03 16:41:46 -07002337 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002338 self.log.debug("Assert final state")
2339 # Make sure no jobs are running
2340 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002341 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002342 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002343 gc.collect()
2344 for obj in gc.get_objects():
2345 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002346 self.log.debug("Leaked git repo object: 0x%x %s" %
2347 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002348 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002349 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002350 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002351 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002352 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002353 for tenant in self.sched.abide.tenants.values():
2354 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002355 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002356 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002357
2358 def shutdown(self):
2359 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002360 self.executor_server.hold_jobs_in_build = False
2361 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002362 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002363 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002364 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002365 self.sched.stop()
2366 self.sched.join()
2367 self.statsd.stop()
2368 self.statsd.join()
2369 self.webapp.stop()
2370 self.webapp.join()
2371 self.rpc.stop()
2372 self.rpc.join()
2373 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002374 self.fake_nodepool.stop()
2375 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002376 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002377 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002378 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002379 # Further the pydevd threads also need to be whitelisted so debugging
2380 # e.g. in PyCharm is possible without breaking shutdown.
2381 whitelist = ['executor-watchdog',
2382 'pydevd.CommandThread',
2383 'pydevd.Reader',
2384 'pydevd.Writer',
2385 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002386 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002387 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002388 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002389 log_str = ""
2390 for thread_id, stack_frame in sys._current_frames().items():
2391 log_str += "Thread: %s\n" % thread_id
2392 log_str += "".join(traceback.format_stack(stack_frame))
2393 self.log.debug(log_str)
2394 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002395
James E. Blaira002b032017-04-18 10:35:48 -07002396 def assertCleanShutdown(self):
2397 pass
2398
James E. Blairc4ba97a2017-04-19 16:26:24 -07002399 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002400 parts = project.split('/')
2401 path = os.path.join(self.upstream_root, *parts[:-1])
2402 if not os.path.exists(path):
2403 os.makedirs(path)
2404 path = os.path.join(self.upstream_root, project)
2405 repo = git.Repo.init(path)
2406
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002407 with repo.config_writer() as config_writer:
2408 config_writer.set_value('user', 'email', 'user@example.com')
2409 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002410
Clark Boylanb640e052014-04-03 16:41:46 -07002411 repo.index.commit('initial commit')
2412 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002413 if tag:
2414 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002415
James E. Blair97d902e2014-08-21 13:25:56 -07002416 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002417 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002418 repo.git.clean('-x', '-f', '-d')
2419
James E. Blair97d902e2014-08-21 13:25:56 -07002420 def create_branch(self, project, branch):
2421 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002422 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002423 fn = os.path.join(path, 'README')
2424
2425 branch_head = repo.create_head(branch)
2426 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002427 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002428 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002429 f.close()
2430 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002431 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002432
James E. Blair97d902e2014-08-21 13:25:56 -07002433 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002434 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002435 repo.git.clean('-x', '-f', '-d')
2436
Sachi King9f16d522016-03-16 12:20:45 +11002437 def create_commit(self, project):
2438 path = os.path.join(self.upstream_root, project)
2439 repo = git.Repo(path)
2440 repo.head.reference = repo.heads['master']
2441 file_name = os.path.join(path, 'README')
2442 with open(file_name, 'a') as f:
2443 f.write('creating fake commit\n')
2444 repo.index.add([file_name])
2445 commit = repo.index.commit('Creating a fake commit')
2446 return commit.hexsha
2447
James E. Blairf4a5f022017-04-18 14:01:10 -07002448 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002449 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002450 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002451 while len(self.builds):
2452 self.release(self.builds[0])
2453 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002454 i += 1
2455 if count is not None and i >= count:
2456 break
James E. Blairb8c16472015-05-05 14:55:26 -07002457
James E. Blairdf25ddc2017-07-08 07:57:09 -07002458 def getSortedBuilds(self):
2459 "Return the list of currently running builds sorted by name"
2460
2461 return sorted(self.builds, key=lambda x: x.name)
2462
Clark Boylanb640e052014-04-03 16:41:46 -07002463 def release(self, job):
2464 if isinstance(job, FakeBuild):
2465 job.release()
2466 else:
2467 job.waiting = False
2468 self.log.debug("Queued job %s released" % job.unique)
2469 self.gearman_server.wakeConnections()
2470
2471 def getParameter(self, job, name):
2472 if isinstance(job, FakeBuild):
2473 return job.parameters[name]
2474 else:
2475 parameters = json.loads(job.arguments)
2476 return parameters[name]
2477
Clark Boylanb640e052014-04-03 16:41:46 -07002478 def haveAllBuildsReported(self):
2479 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002480 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002481 return False
2482 # Find out if every build that the worker has completed has been
2483 # reported back to Zuul. If it hasn't then that means a Gearman
2484 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002485 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002486 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002487 if not zbuild:
2488 # It has already been reported
2489 continue
2490 # It hasn't been reported yet.
2491 return False
2492 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002493 worker = self.executor_server.executor_worker
2494 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002495 if connection.state == 'GRAB_WAIT':
2496 return False
2497 return True
2498
2499 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002500 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002501 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002502 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002503 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002504 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002505 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002506 for j in conn.related_jobs.values():
2507 if j.unique == build.uuid:
2508 client_job = j
2509 break
2510 if not client_job:
2511 self.log.debug("%s is not known to the gearman client" %
2512 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002513 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002514 if not client_job.handle:
2515 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002516 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002517 server_job = self.gearman_server.jobs.get(client_job.handle)
2518 if not server_job:
2519 self.log.debug("%s is not known to the gearman server" %
2520 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002521 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002522 if not hasattr(server_job, 'waiting'):
2523 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002524 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002525 if server_job.waiting:
2526 continue
James E. Blair17302972016-08-10 16:11:42 -07002527 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002528 self.log.debug("%s has not reported start" % build)
2529 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002530 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002531 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002532 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002533 if worker_build:
2534 if worker_build.isWaiting():
2535 continue
2536 else:
2537 self.log.debug("%s is running" % worker_build)
2538 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002539 else:
James E. Blair962220f2016-08-03 11:22:38 -07002540 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002541 return False
James E. Blaira002b032017-04-18 10:35:48 -07002542 for (build_uuid, job_worker) in \
2543 self.executor_server.job_workers.items():
2544 if build_uuid not in seen_builds:
2545 self.log.debug("%s is not finalized" % build_uuid)
2546 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002547 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002548
James E. Blairdce6cea2016-12-20 16:45:32 -08002549 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002550 if self.fake_nodepool.paused:
2551 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002552 if self.sched.nodepool.requests:
2553 return False
2554 return True
2555
Jan Hruban6b71aff2015-10-22 16:58:08 +02002556 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002557 for event_queue in self.event_queues:
2558 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002559
2560 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002561 for event_queue in self.event_queues:
2562 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002563
Clark Boylanb640e052014-04-03 16:41:46 -07002564 def waitUntilSettled(self):
2565 self.log.debug("Waiting until settled...")
2566 start = time.time()
2567 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002568 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002569 self.log.error("Timeout waiting for Zuul to settle")
2570 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002571 for event_queue in self.event_queues:
2572 self.log.error(" %s: %s" %
2573 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002574 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002575 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002576 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002577 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002578 self.log.error("All requests completed: %s" %
2579 (self.areAllNodeRequestsComplete(),))
2580 self.log.error("Merge client jobs: %s" %
2581 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002582 raise Exception("Timeout waiting for Zuul to settle")
2583 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002584
Paul Belanger174a8272017-03-14 13:20:10 -04002585 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002586 # have all build states propogated to zuul?
2587 if self.haveAllBuildsReported():
2588 # Join ensures that the queue is empty _and_ events have been
2589 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002590 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002591 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002592 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002593 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002594 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002595 self.areAllNodeRequestsComplete() and
2596 all(self.eventQueuesEmpty())):
2597 # The queue empty check is placed at the end to
2598 # ensure that if a component adds an event between
2599 # when locked the run handler and checked that the
2600 # components were stable, we don't erroneously
2601 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002602 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002603 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002604 self.log.debug("...settled.")
2605 return
2606 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002607 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002608 self.sched.wake_event.wait(0.1)
2609
2610 def countJobResults(self, jobs, result):
2611 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002612 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002613
Monty Taylor0d926122017-05-24 08:07:56 -05002614 def getBuildByName(self, name):
2615 for build in self.builds:
2616 if build.name == name:
2617 return build
2618 raise Exception("Unable to find build %s" % name)
2619
James E. Blair96c6bf82016-01-15 16:20:40 -08002620 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002621 for job in self.history:
2622 if (job.name == name and
2623 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002624 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002625 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002626 raise Exception("Unable to find job %s in history" % name)
2627
2628 def assertEmptyQueues(self):
2629 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002630 for tenant in self.sched.abide.tenants.values():
2631 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002632 for pipeline_queue in pipeline.queues:
2633 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002634 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002635 pipeline.name, pipeline_queue.name,
2636 pipeline_queue.queue))
2637 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002638 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002639
2640 def assertReportedStat(self, key, value=None, kind=None):
2641 start = time.time()
2642 while time.time() < (start + 5):
2643 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002644 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002645 if key == k:
2646 if value is None and kind is None:
2647 return
2648 elif value:
2649 if value == v:
2650 return
2651 elif kind:
2652 if v.endswith('|' + kind):
2653 return
2654 time.sleep(0.1)
2655
Clark Boylanb640e052014-04-03 16:41:46 -07002656 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002657
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002658 def assertBuilds(self, builds):
2659 """Assert that the running builds are as described.
2660
2661 The list of running builds is examined and must match exactly
2662 the list of builds described by the input.
2663
2664 :arg list builds: A list of dictionaries. Each item in the
2665 list must match the corresponding build in the build
2666 history, and each element of the dictionary must match the
2667 corresponding attribute of the build.
2668
2669 """
James E. Blair3158e282016-08-19 09:34:11 -07002670 try:
2671 self.assertEqual(len(self.builds), len(builds))
2672 for i, d in enumerate(builds):
2673 for k, v in d.items():
2674 self.assertEqual(
2675 getattr(self.builds[i], k), v,
2676 "Element %i in builds does not match" % (i,))
2677 except Exception:
2678 for build in self.builds:
2679 self.log.error("Running build: %s" % build)
2680 else:
2681 self.log.error("No running builds")
2682 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002683
James E. Blairb536ecc2016-08-31 10:11:42 -07002684 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002685 """Assert that the completed builds are as described.
2686
2687 The list of completed builds is examined and must match
2688 exactly the list of builds described by the input.
2689
2690 :arg list history: A list of dictionaries. Each item in the
2691 list must match the corresponding build in the build
2692 history, and each element of the dictionary must match the
2693 corresponding attribute of the build.
2694
James E. Blairb536ecc2016-08-31 10:11:42 -07002695 :arg bool ordered: If true, the history must match the order
2696 supplied, if false, the builds are permitted to have
2697 arrived in any order.
2698
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002699 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002700 def matches(history_item, item):
2701 for k, v in item.items():
2702 if getattr(history_item, k) != v:
2703 return False
2704 return True
James E. Blair3158e282016-08-19 09:34:11 -07002705 try:
2706 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002707 if ordered:
2708 for i, d in enumerate(history):
2709 if not matches(self.history[i], d):
2710 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002711 "Element %i in history does not match %s" %
2712 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002713 else:
2714 unseen = self.history[:]
2715 for i, d in enumerate(history):
2716 found = False
2717 for unseen_item in unseen:
2718 if matches(unseen_item, d):
2719 found = True
2720 unseen.remove(unseen_item)
2721 break
2722 if not found:
2723 raise Exception("No match found for element %i "
2724 "in history" % (i,))
2725 if unseen:
2726 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002727 except Exception:
2728 for build in self.history:
2729 self.log.error("Completed build: %s" % build)
2730 else:
2731 self.log.error("No completed builds")
2732 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002733
James E. Blair6ac368c2016-12-22 18:07:20 -08002734 def printHistory(self):
2735 """Log the build history.
2736
2737 This can be useful during tests to summarize what jobs have
2738 completed.
2739
2740 """
2741 self.log.debug("Build history:")
2742 for build in self.history:
2743 self.log.debug(build)
2744
James E. Blair59fdbac2015-12-07 17:08:06 -08002745 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002746 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2747
James E. Blair9ea70072017-04-19 16:05:30 -07002748 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002749 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002750 if not os.path.exists(root):
2751 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002752 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2753 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002754- tenant:
2755 name: openstack
2756 source:
2757 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002758 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002759 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002760 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002761 - org/project
2762 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002763 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002764 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002765 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002766 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002767 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002768
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002769 def addCommitToRepo(self, project, message, files,
2770 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002771 path = os.path.join(self.upstream_root, project)
2772 repo = git.Repo(path)
2773 repo.head.reference = branch
2774 zuul.merger.merger.reset_repo_to_head(repo)
2775 for fn, content in files.items():
2776 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002777 try:
2778 os.makedirs(os.path.dirname(fn))
2779 except OSError:
2780 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002781 with open(fn, 'w') as f:
2782 f.write(content)
2783 repo.index.add([fn])
2784 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002785 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002786 repo.heads[branch].commit = commit
2787 repo.head.reference = branch
2788 repo.git.clean('-x', '-f', '-d')
2789 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002790 if tag:
2791 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002792 return before
2793
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002794 def commitConfigUpdate(self, project_name, source_name):
2795 """Commit an update to zuul.yaml
2796
2797 This overwrites the zuul.yaml in the specificed project with
2798 the contents specified.
2799
2800 :arg str project_name: The name of the project containing
2801 zuul.yaml (e.g., common-config)
2802
2803 :arg str source_name: The path to the file (underneath the
2804 test fixture directory) whose contents should be used to
2805 replace zuul.yaml.
2806 """
2807
2808 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002809 files = {}
2810 with open(source_path, 'r') as f:
2811 data = f.read()
2812 layout = yaml.safe_load(data)
2813 files['zuul.yaml'] = data
2814 for item in layout:
2815 if 'job' in item:
2816 jobname = item['job']['name']
2817 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002818 before = self.addCommitToRepo(
2819 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002820 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002821 return before
2822
Clint Byrum627ba362017-08-14 13:20:40 -07002823 def newTenantConfig(self, source_name):
2824 """ Use this to update the tenant config file in tests
2825
2826 This will update self.tenant_config_file to point to a temporary file
2827 for the duration of this particular test. The content of that file will
2828 be taken from FIXTURE_DIR/source_name
2829
2830 After the test the original value of self.tenant_config_file will be
2831 restored.
2832
2833 :arg str source_name: The path of the file under
2834 FIXTURE_DIR that will be used to populate the new tenant
2835 config file.
2836 """
2837 source_path = os.path.join(FIXTURE_DIR, source_name)
2838 orig_tenant_config_file = self.tenant_config_file
2839 with tempfile.NamedTemporaryFile(
2840 delete=False, mode='wb') as new_tenant_config:
2841 self.tenant_config_file = new_tenant_config.name
2842 with open(source_path, mode='rb') as source_tenant_config:
2843 new_tenant_config.write(source_tenant_config.read())
2844 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2845 self.setupAllProjectKeys()
2846 self.log.debug(
2847 'tenant_config_file = {}'.format(self.tenant_config_file))
2848
2849 def _restoreTenantConfig():
2850 self.log.debug(
2851 'restoring tenant_config_file = {}'.format(
2852 orig_tenant_config_file))
2853 os.unlink(self.tenant_config_file)
2854 self.tenant_config_file = orig_tenant_config_file
2855 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2856 self.addCleanup(_restoreTenantConfig)
2857
James E. Blair7fc8daa2016-08-08 15:37:15 -07002858 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002859
James E. Blair7fc8daa2016-08-08 15:37:15 -07002860 """Inject a Fake (Gerrit) event.
2861
2862 This method accepts a JSON-encoded event and simulates Zuul
2863 having received it from Gerrit. It could (and should)
2864 eventually apply to any connection type, but is currently only
2865 used with Gerrit connections. The name of the connection is
2866 used to look up the corresponding server, and the event is
2867 simulated as having been received by all Zuul connections
2868 attached to that server. So if two Gerrit connections in Zuul
2869 are connected to the same Gerrit server, and you invoke this
2870 method specifying the name of one of them, the event will be
2871 received by both.
2872
2873 .. note::
2874
2875 "self.fake_gerrit.addEvent" calls should be migrated to
2876 this method.
2877
2878 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002879 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002880 :arg str event: The JSON-encoded event.
2881
2882 """
2883 specified_conn = self.connections.connections[connection]
2884 for conn in self.connections.connections.values():
2885 if (isinstance(conn, specified_conn.__class__) and
2886 specified_conn.server == conn.server):
2887 conn.addEvent(event)
2888
James E. Blaird8af5422017-05-24 13:59:40 -07002889 def getUpstreamRepos(self, projects):
2890 """Return upstream git repo objects for the listed projects
2891
2892 :arg list projects: A list of strings, each the canonical name
2893 of a project.
2894
2895 :returns: A dictionary of {name: repo} for every listed
2896 project.
2897 :rtype: dict
2898
2899 """
2900
2901 repos = {}
2902 for project in projects:
2903 # FIXME(jeblair): the upstream root does not yet have a
2904 # hostname component; that needs to be added, and this
2905 # line removed:
2906 tmp_project_name = '/'.join(project.split('/')[1:])
2907 path = os.path.join(self.upstream_root, tmp_project_name)
2908 repo = git.Repo(path)
2909 repos[project] = repo
2910 return repos
2911
James E. Blair3f876d52016-07-22 13:07:14 -07002912
2913class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002914 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002915 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002916
Jamie Lennox7655b552017-03-17 12:33:38 +11002917 @contextmanager
2918 def jobLog(self, build):
2919 """Print job logs on assertion errors
2920
2921 This method is a context manager which, if it encounters an
2922 ecxeption, adds the build log to the debug output.
2923
2924 :arg Build build: The build that's being asserted.
2925 """
2926 try:
2927 yield
2928 except Exception:
2929 path = os.path.join(self.test_root, build.uuid,
2930 'work', 'logs', 'job-output.txt')
2931 with open(path) as f:
2932 self.log.debug(f.read())
2933 raise
2934
Joshua Heskethd78b4482015-09-14 16:56:34 -06002935
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002936class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002937 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002938 use_ssl = True
2939
2940
Joshua Heskethd78b4482015-09-14 16:56:34 -06002941class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002942 def setup_config(self):
2943 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002944 for section_name in self.config.sections():
2945 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2946 section_name, re.I)
2947 if not con_match:
2948 continue
2949
2950 if self.config.get(section_name, 'driver') == 'sql':
2951 f = MySQLSchemaFixture()
2952 self.useFixture(f)
2953 if (self.config.get(section_name, 'dburi') ==
2954 '$MYSQL_FIXTURE_DBURI$'):
2955 self.config.set(section_name, 'dburi', f.dburi)