blob: 5841c08d9fc75b899991dfcb6b6c8adb0e0f9038 [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:
James E. Blairf641f4f2017-09-27 10:10:54 -07001488 if playbook.path:
1489 result = build.run()
1490 else:
1491 result = (self.RESULT_NORMAL, 0)
James E. Blaire1767bc2016-08-02 10:00:27 -07001492 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001493
James E. Blairad8dca02017-02-21 11:48:32 -05001494 def getHostList(self, args):
1495 self.log.debug("hostlist")
1496 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001497 for host in hosts:
1498 host['host_vars']['ansible_connection'] = 'local'
1499
1500 hosts.append(dict(
1501 name='localhost',
1502 host_vars=dict(ansible_connection='local'),
1503 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001504 return hosts
1505
James E. Blairf5dbd002015-12-23 15:26:17 -08001506
Clark Boylanb640e052014-04-03 16:41:46 -07001507class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001508 """A Gearman server for use in tests.
1509
1510 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1511 added to the queue but will not be distributed to workers
1512 until released. This attribute may be changed at any time and
1513 will take effect for subsequently enqueued jobs, but
1514 previously held jobs will still need to be explicitly
1515 released.
1516
1517 """
1518
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001519 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001520 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001521 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001522 if use_ssl:
1523 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1524 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1525 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1526 else:
1527 ssl_ca = None
1528 ssl_cert = None
1529 ssl_key = None
1530
1531 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1532 ssl_cert=ssl_cert,
1533 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001534
1535 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001536 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1537 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001538 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001539 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001540 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001541 elif job.name.startswith(b'merger:'):
1542 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001543 else:
1544 job.waiting = False
1545 if job.waiting:
1546 continue
1547 if job.name in connection.functions:
1548 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001549 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001550 connection.related_jobs[job.handle] = job
1551 job.worker_connection = connection
1552 job.running = True
1553 return job
1554 return None
1555
1556 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001557 """Release a held job.
1558
1559 :arg str regex: A regular expression which, if supplied, will
1560 cause only jobs with matching names to be released. If
1561 not supplied, all jobs will be released.
1562 """
Clark Boylanb640e052014-04-03 16:41:46 -07001563 released = False
1564 qlen = (len(self.high_queue) + len(self.normal_queue) +
1565 len(self.low_queue))
1566 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1567 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001568 match = False
1569 if job.name == b'executor:execute':
1570 parameters = json.loads(job.arguments.decode('utf8'))
1571 if not regex or re.match(regex, parameters.get('job')):
1572 match = True
1573 if job.name == b'merger:merge':
1574 if not regex:
1575 match = True
1576 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001577 self.log.debug("releasing queued job %s" %
1578 job.unique)
1579 job.waiting = False
1580 released = True
1581 else:
1582 self.log.debug("not releasing queued job %s" %
1583 job.unique)
1584 if released:
1585 self.wakeConnections()
1586 qlen = (len(self.high_queue) + len(self.normal_queue) +
1587 len(self.low_queue))
1588 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1589
1590
1591class FakeSMTP(object):
1592 log = logging.getLogger('zuul.FakeSMTP')
1593
1594 def __init__(self, messages, server, port):
1595 self.server = server
1596 self.port = port
1597 self.messages = messages
1598
1599 def sendmail(self, from_email, to_email, msg):
1600 self.log.info("Sending email from %s, to %s, with msg %s" % (
1601 from_email, to_email, msg))
1602
1603 headers = msg.split('\n\n', 1)[0]
1604 body = msg.split('\n\n', 1)[1]
1605
1606 self.messages.append(dict(
1607 from_email=from_email,
1608 to_email=to_email,
1609 msg=msg,
1610 headers=headers,
1611 body=body,
1612 ))
1613
1614 return True
1615
1616 def quit(self):
1617 return True
1618
1619
James E. Blairdce6cea2016-12-20 16:45:32 -08001620class FakeNodepool(object):
1621 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001622 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001623
1624 log = logging.getLogger("zuul.test.FakeNodepool")
1625
1626 def __init__(self, host, port, chroot):
1627 self.client = kazoo.client.KazooClient(
1628 hosts='%s:%s%s' % (host, port, chroot))
1629 self.client.start()
1630 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001631 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001632 self.thread = threading.Thread(target=self.run)
1633 self.thread.daemon = True
1634 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001635 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001636
1637 def stop(self):
1638 self._running = False
1639 self.thread.join()
1640 self.client.stop()
1641 self.client.close()
1642
1643 def run(self):
1644 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001645 try:
1646 self._run()
1647 except Exception:
1648 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001649 time.sleep(0.1)
1650
1651 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001652 if self.paused:
1653 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001654 for req in self.getNodeRequests():
1655 self.fulfillRequest(req)
1656
1657 def getNodeRequests(self):
1658 try:
1659 reqids = self.client.get_children(self.REQUEST_ROOT)
1660 except kazoo.exceptions.NoNodeError:
1661 return []
1662 reqs = []
1663 for oid in sorted(reqids):
1664 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001665 try:
1666 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001667 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001668 data['_oid'] = oid
1669 reqs.append(data)
1670 except kazoo.exceptions.NoNodeError:
1671 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001672 return reqs
1673
James E. Blaire18d4602017-01-05 11:17:28 -08001674 def getNodes(self):
1675 try:
1676 nodeids = self.client.get_children(self.NODE_ROOT)
1677 except kazoo.exceptions.NoNodeError:
1678 return []
1679 nodes = []
1680 for oid in sorted(nodeids):
1681 path = self.NODE_ROOT + '/' + oid
1682 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001683 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001684 data['_oid'] = oid
1685 try:
1686 lockfiles = self.client.get_children(path + '/lock')
1687 except kazoo.exceptions.NoNodeError:
1688 lockfiles = []
1689 if lockfiles:
1690 data['_lock'] = True
1691 else:
1692 data['_lock'] = False
1693 nodes.append(data)
1694 return nodes
1695
James E. Blaira38c28e2017-01-04 10:33:20 -08001696 def makeNode(self, request_id, node_type):
1697 now = time.time()
1698 path = '/nodepool/nodes/'
1699 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001700 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001701 provider='test-provider',
1702 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001703 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001704 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001705 public_ipv4='127.0.0.1',
1706 private_ipv4=None,
1707 public_ipv6=None,
1708 allocated_to=request_id,
1709 state='ready',
1710 state_time=now,
1711 created_time=now,
1712 updated_time=now,
1713 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001714 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001715 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001716 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001717 path = self.client.create(path, data,
1718 makepath=True,
1719 sequence=True)
1720 nodeid = path.split("/")[-1]
1721 return nodeid
1722
James E. Blair6ab79e02017-01-06 10:10:17 -08001723 def addFailRequest(self, request):
1724 self.fail_requests.add(request['_oid'])
1725
James E. Blairdce6cea2016-12-20 16:45:32 -08001726 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001727 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001728 return
1729 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001730 oid = request['_oid']
1731 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001732
James E. Blair6ab79e02017-01-06 10:10:17 -08001733 if oid in self.fail_requests:
1734 request['state'] = 'failed'
1735 else:
1736 request['state'] = 'fulfilled'
1737 nodes = []
1738 for node in request['node_types']:
1739 nodeid = self.makeNode(oid, node)
1740 nodes.append(nodeid)
1741 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001742
James E. Blaira38c28e2017-01-04 10:33:20 -08001743 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001744 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001745 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001746 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001747 try:
1748 self.client.set(path, data)
1749 except kazoo.exceptions.NoNodeError:
1750 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001751
1752
James E. Blair498059b2016-12-20 13:50:13 -08001753class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001754 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001755 super(ChrootedKazooFixture, self).__init__()
1756
1757 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1758 if ':' in zk_host:
1759 host, port = zk_host.split(':')
1760 else:
1761 host = zk_host
1762 port = None
1763
1764 self.zookeeper_host = host
1765
1766 if not port:
1767 self.zookeeper_port = 2181
1768 else:
1769 self.zookeeper_port = int(port)
1770
Clark Boylan621ec9a2017-04-07 17:41:33 -07001771 self.test_id = test_id
1772
James E. Blair498059b2016-12-20 13:50:13 -08001773 def _setUp(self):
1774 # Make sure the test chroot paths do not conflict
1775 random_bits = ''.join(random.choice(string.ascii_lowercase +
1776 string.ascii_uppercase)
1777 for x in range(8))
1778
Clark Boylan621ec9a2017-04-07 17:41:33 -07001779 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001780 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1781
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001782 self.addCleanup(self._cleanup)
1783
James E. Blair498059b2016-12-20 13:50:13 -08001784 # Ensure the chroot path exists and clean up any pre-existing znodes.
1785 _tmp_client = kazoo.client.KazooClient(
1786 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1787 _tmp_client.start()
1788
1789 if _tmp_client.exists(self.zookeeper_chroot):
1790 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1791
1792 _tmp_client.ensure_path(self.zookeeper_chroot)
1793 _tmp_client.stop()
1794 _tmp_client.close()
1795
James E. Blair498059b2016-12-20 13:50:13 -08001796 def _cleanup(self):
1797 '''Remove the chroot path.'''
1798 # Need a non-chroot'ed client to remove the chroot path
1799 _tmp_client = kazoo.client.KazooClient(
1800 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1801 _tmp_client.start()
1802 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1803 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001804 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001805
1806
Joshua Heskethd78b4482015-09-14 16:56:34 -06001807class MySQLSchemaFixture(fixtures.Fixture):
1808 def setUp(self):
1809 super(MySQLSchemaFixture, self).setUp()
1810
1811 random_bits = ''.join(random.choice(string.ascii_lowercase +
1812 string.ascii_uppercase)
1813 for x in range(8))
1814 self.name = '%s_%s' % (random_bits, os.getpid())
1815 self.passwd = uuid.uuid4().hex
1816 db = pymysql.connect(host="localhost",
1817 user="openstack_citest",
1818 passwd="openstack_citest",
1819 db="openstack_citest")
1820 cur = db.cursor()
1821 cur.execute("create database %s" % self.name)
1822 cur.execute(
1823 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1824 (self.name, self.name, self.passwd))
1825 cur.execute("flush privileges")
1826
1827 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1828 self.passwd,
1829 self.name)
1830 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1831 self.addCleanup(self.cleanup)
1832
1833 def cleanup(self):
1834 db = pymysql.connect(host="localhost",
1835 user="openstack_citest",
1836 passwd="openstack_citest",
1837 db="openstack_citest")
1838 cur = db.cursor()
1839 cur.execute("drop database %s" % self.name)
1840 cur.execute("drop user '%s'@'localhost'" % self.name)
1841 cur.execute("flush privileges")
1842
1843
Maru Newby3fe5f852015-01-13 04:22:14 +00001844class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001845 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001846 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001847
James E. Blair1c236df2017-02-01 14:07:24 -08001848 def attachLogs(self, *args):
1849 def reader():
1850 self._log_stream.seek(0)
1851 while True:
1852 x = self._log_stream.read(4096)
1853 if not x:
1854 break
1855 yield x.encode('utf8')
1856 content = testtools.content.content_from_reader(
1857 reader,
1858 testtools.content_type.UTF8_TEXT,
1859 False)
1860 self.addDetail('logging', content)
1861
Clark Boylanb640e052014-04-03 16:41:46 -07001862 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001863 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001864 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1865 try:
1866 test_timeout = int(test_timeout)
1867 except ValueError:
1868 # If timeout value is invalid do not set a timeout.
1869 test_timeout = 0
1870 if test_timeout > 0:
1871 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1872
1873 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1874 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1875 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1876 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1877 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1878 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1879 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1880 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1881 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1882 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001883 self._log_stream = StringIO()
1884 self.addOnException(self.attachLogs)
1885 else:
1886 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001887
James E. Blair73b41772017-05-22 13:22:55 -07001888 # NOTE(jeblair): this is temporary extra debugging to try to
1889 # track down a possible leak.
1890 orig_git_repo_init = git.Repo.__init__
1891
1892 def git_repo_init(myself, *args, **kw):
1893 orig_git_repo_init(myself, *args, **kw)
1894 self.log.debug("Created git repo 0x%x %s" %
1895 (id(myself), repr(myself)))
1896
1897 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1898 git_repo_init))
1899
James E. Blair1c236df2017-02-01 14:07:24 -08001900 handler = logging.StreamHandler(self._log_stream)
1901 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1902 '%(levelname)-8s %(message)s')
1903 handler.setFormatter(formatter)
1904
1905 logger = logging.getLogger()
1906 logger.setLevel(logging.DEBUG)
1907 logger.addHandler(handler)
1908
Clark Boylan3410d532017-04-25 12:35:29 -07001909 # Make sure we don't carry old handlers around in process state
1910 # which slows down test runs
1911 self.addCleanup(logger.removeHandler, handler)
1912 self.addCleanup(handler.close)
1913 self.addCleanup(handler.flush)
1914
James E. Blair1c236df2017-02-01 14:07:24 -08001915 # NOTE(notmorgan): Extract logging overrides for specific
1916 # libraries from the OS_LOG_DEFAULTS env and create loggers
1917 # for each. This is used to limit the output during test runs
1918 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001919 log_defaults_from_env = os.environ.get(
1920 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001921 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001922
James E. Blairdce6cea2016-12-20 16:45:32 -08001923 if log_defaults_from_env:
1924 for default in log_defaults_from_env.split(','):
1925 try:
1926 name, level_str = default.split('=', 1)
1927 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001928 logger = logging.getLogger(name)
1929 logger.setLevel(level)
1930 logger.addHandler(handler)
1931 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001932 except ValueError:
1933 # NOTE(notmorgan): Invalid format of the log default,
1934 # skip and don't try and apply a logger for the
1935 # specified module
1936 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001937
Maru Newby3fe5f852015-01-13 04:22:14 +00001938
1939class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001940 """A test case with a functioning Zuul.
1941
1942 The following class variables are used during test setup and can
1943 be overidden by subclasses but are effectively read-only once a
1944 test method starts running:
1945
1946 :cvar str config_file: This points to the main zuul config file
1947 within the fixtures directory. Subclasses may override this
1948 to obtain a different behavior.
1949
1950 :cvar str tenant_config_file: This is the tenant config file
1951 (which specifies from what git repos the configuration should
1952 be loaded). It defaults to the value specified in
1953 `config_file` but can be overidden by subclasses to obtain a
1954 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001955 configuration. See also the :py:func:`simple_layout`
1956 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001957
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001958 :cvar bool create_project_keys: Indicates whether Zuul should
1959 auto-generate keys for each project, or whether the test
1960 infrastructure should insert dummy keys to save time during
1961 startup. Defaults to False.
1962
James E. Blaire7b99a02016-08-05 14:27:34 -07001963 The following are instance variables that are useful within test
1964 methods:
1965
1966 :ivar FakeGerritConnection fake_<connection>:
1967 A :py:class:`~tests.base.FakeGerritConnection` will be
1968 instantiated for each connection present in the config file
1969 and stored here. For instance, `fake_gerrit` will hold the
1970 FakeGerritConnection object for a connection named `gerrit`.
1971
1972 :ivar FakeGearmanServer gearman_server: An instance of
1973 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1974 server that all of the Zuul components in this test use to
1975 communicate with each other.
1976
Paul Belanger174a8272017-03-14 13:20:10 -04001977 :ivar RecordingExecutorServer executor_server: An instance of
1978 :py:class:`~tests.base.RecordingExecutorServer` which is the
1979 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001980
1981 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1982 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001983 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001984 list upon completion.
1985
1986 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1987 objects representing completed builds. They are appended to
1988 the list in the order they complete.
1989
1990 """
1991
James E. Blair83005782015-12-11 14:46:03 -08001992 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001993 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001994 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001995 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001996
1997 def _startMerger(self):
1998 self.merge_server = zuul.merger.server.MergeServer(self.config,
1999 self.connections)
2000 self.merge_server.start()
2001
Maru Newby3fe5f852015-01-13 04:22:14 +00002002 def setUp(self):
2003 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08002004
2005 self.setupZK()
2006
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002007 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07002008 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002009 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2010 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002011 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002012 tmp_root = tempfile.mkdtemp(
2013 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002014 self.test_root = os.path.join(tmp_root, "zuul-test")
2015 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002016 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002017 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002018 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002019 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2020 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002021
2022 if os.path.exists(self.test_root):
2023 shutil.rmtree(self.test_root)
2024 os.makedirs(self.test_root)
2025 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002026 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002027 os.makedirs(self.merger_state_root)
2028 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002029
2030 # Make per test copy of Configuration.
2031 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002032 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2033 if not os.path.exists(self.private_key_file):
2034 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2035 shutil.copy(src_private_key_file, self.private_key_file)
2036 shutil.copy('{}.pub'.format(src_private_key_file),
2037 '{}.pub'.format(self.private_key_file))
2038 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002039 self.config.set('scheduler', 'tenant_config',
2040 os.path.join(
2041 FIXTURE_DIR,
2042 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002043 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002044 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002045 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002046 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002047 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002048
Clark Boylanb640e052014-04-03 16:41:46 -07002049 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10002050 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
2051 # see: https://github.com/jsocol/pystatsd/issues/61
2052 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07002053 os.environ['STATSD_PORT'] = str(self.statsd.port)
2054 self.statsd.start()
2055 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05002056 importlib.reload(statsd)
2057 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07002058
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002059 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002060
2061 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002062 self.log.info("Gearman server on port %s" %
2063 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002064 if self.use_ssl:
2065 self.log.info('SSL enabled for gearman')
2066 self.config.set(
2067 'gearman', 'ssl_ca',
2068 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2069 self.config.set(
2070 'gearman', 'ssl_cert',
2071 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2072 self.config.set(
2073 'gearman', 'ssl_key',
2074 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002075
James E. Blaire511d2f2016-12-08 15:22:26 -08002076 gerritsource.GerritSource.replication_timeout = 1.5
2077 gerritsource.GerritSource.replication_retry_interval = 0.5
2078 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002079
Joshua Hesketh352264b2015-08-11 23:42:08 +10002080 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07002081
Jan Hruban7083edd2015-08-21 14:00:54 +02002082 self.webapp = zuul.webapp.WebApp(
2083 self.sched, port=0, listen_address='127.0.0.1')
2084
Jan Hruban6b71aff2015-10-22 16:58:08 +02002085 self.event_queues = [
2086 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002087 self.sched.trigger_event_queue,
2088 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002089 ]
2090
James E. Blairfef78942016-03-11 16:28:56 -08002091 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002092 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002093
Paul Belanger174a8272017-03-14 13:20:10 -04002094 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002095 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002096 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002097 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002098 _test_root=self.test_root,
2099 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002100 self.executor_server.start()
2101 self.history = self.executor_server.build_history
2102 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002103
Paul Belanger174a8272017-03-14 13:20:10 -04002104 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002105 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002106 self.merge_client = zuul.merger.client.MergeClient(
2107 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002108 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002109 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002110 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002111
James E. Blair0d5a36e2017-02-21 10:53:44 -05002112 self.fake_nodepool = FakeNodepool(
2113 self.zk_chroot_fixture.zookeeper_host,
2114 self.zk_chroot_fixture.zookeeper_port,
2115 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002116
Paul Belanger174a8272017-03-14 13:20:10 -04002117 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002118 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002119 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002120 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002121
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002122 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002123
2124 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002125 self.webapp.start()
2126 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002127 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002128 # Cleanups are run in reverse order
2129 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002130 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002131 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002132
James E. Blairb9c0d772017-03-03 14:34:49 -08002133 self.sched.reconfigure(self.config)
2134 self.sched.resume()
2135
Tobias Henkel7df274b2017-05-26 17:41:11 +02002136 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002137 # Set up gerrit related fakes
2138 # Set a changes database so multiple FakeGerrit's can report back to
2139 # a virtual canonical database given by the configured hostname
2140 self.gerrit_changes_dbs = {}
2141
2142 def getGerritConnection(driver, name, config):
2143 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2144 con = FakeGerritConnection(driver, name, config,
2145 changes_db=db,
2146 upstream_root=self.upstream_root)
2147 self.event_queues.append(con.event_queue)
2148 setattr(self, 'fake_' + name, con)
2149 return con
2150
2151 self.useFixture(fixtures.MonkeyPatch(
2152 'zuul.driver.gerrit.GerritDriver.getConnection',
2153 getGerritConnection))
2154
Gregory Haynes4fc12542015-04-22 20:38:06 -07002155 def getGithubConnection(driver, name, config):
2156 con = FakeGithubConnection(driver, name, config,
2157 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002158 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002159 setattr(self, 'fake_' + name, con)
2160 return con
2161
2162 self.useFixture(fixtures.MonkeyPatch(
2163 'zuul.driver.github.GithubDriver.getConnection',
2164 getGithubConnection))
2165
James E. Blaire511d2f2016-12-08 15:22:26 -08002166 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002167 # TODO(jhesketh): This should come from lib.connections for better
2168 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002169 # Register connections from the config
2170 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002171
Joshua Hesketh352264b2015-08-11 23:42:08 +10002172 def FakeSMTPFactory(*args, **kw):
2173 args = [self.smtp_messages] + list(args)
2174 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002175
Joshua Hesketh352264b2015-08-11 23:42:08 +10002176 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002177
James E. Blaire511d2f2016-12-08 15:22:26 -08002178 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002179 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002180 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002181
James E. Blair83005782015-12-11 14:46:03 -08002182 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002183 # This creates the per-test configuration object. It can be
2184 # overriden by subclasses, but should not need to be since it
2185 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002186 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002187 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002188
James E. Blair39840362017-06-23 20:34:02 +01002189 sections = ['zuul', 'scheduler', 'executor', 'merger']
2190 for section in sections:
2191 if not self.config.has_section(section):
2192 self.config.add_section(section)
2193
James E. Blair06cc3922017-04-19 10:08:10 -07002194 if not self.setupSimpleLayout():
2195 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002196 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002197 self.tenant_config_file)
2198 git_path = os.path.join(
2199 os.path.dirname(
2200 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2201 'git')
2202 if os.path.exists(git_path):
2203 for reponame in os.listdir(git_path):
2204 project = reponame.replace('_', '/')
2205 self.copyDirToRepo(project,
2206 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002207 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002208 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002209 self.setupAllProjectKeys()
2210
James E. Blair06cc3922017-04-19 10:08:10 -07002211 def setupSimpleLayout(self):
2212 # If the test method has been decorated with a simple_layout,
2213 # use that instead of the class tenant_config_file. Set up a
2214 # single config-project with the specified layout, and
2215 # initialize repos for all of the 'project' entries which
2216 # appear in the layout.
2217 test_name = self.id().split('.')[-1]
2218 test = getattr(self, test_name)
2219 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002220 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002221 else:
2222 return False
2223
James E. Blairb70e55a2017-04-19 12:57:02 -07002224 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002225 path = os.path.join(FIXTURE_DIR, path)
2226 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002227 data = f.read()
2228 layout = yaml.safe_load(data)
2229 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002230 untrusted_projects = []
2231 for item in layout:
2232 if 'project' in item:
2233 name = item['project']['name']
2234 untrusted_projects.append(name)
2235 self.init_repo(name)
2236 self.addCommitToRepo(name, 'initial commit',
2237 files={'README': ''},
2238 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002239 if 'job' in item:
2240 jobname = item['job']['name']
2241 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002242
2243 root = os.path.join(self.test_root, "config")
2244 if not os.path.exists(root):
2245 os.makedirs(root)
2246 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2247 config = [{'tenant':
2248 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002249 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002250 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002251 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002252 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002253 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002254 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002255 os.path.join(FIXTURE_DIR, f.name))
2256
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002257 self.init_repo('org/common-config')
2258 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002259 files, branch='master', tag='init')
2260
2261 return True
2262
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002263 def setupAllProjectKeys(self):
2264 if self.create_project_keys:
2265 return
2266
James E. Blair39840362017-06-23 20:34:02 +01002267 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002268 with open(os.path.join(FIXTURE_DIR, path)) as f:
2269 tenant_config = yaml.safe_load(f.read())
2270 for tenant in tenant_config:
2271 sources = tenant['tenant']['source']
2272 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002273 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002274 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002275 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002276 self.setupProjectKeys(source, project)
2277
2278 def setupProjectKeys(self, source, project):
2279 # Make sure we set up an RSA key for the project so that we
2280 # don't spend time generating one:
2281
James E. Blair6459db12017-06-29 14:57:20 -07002282 if isinstance(project, dict):
2283 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002284 key_root = os.path.join(self.state_root, 'keys')
2285 if not os.path.isdir(key_root):
2286 os.mkdir(key_root, 0o700)
2287 private_key_file = os.path.join(key_root, source, project + '.pem')
2288 private_key_dir = os.path.dirname(private_key_file)
2289 self.log.debug("Installing test keys for project %s at %s" % (
2290 project, private_key_file))
2291 if not os.path.isdir(private_key_dir):
2292 os.makedirs(private_key_dir)
2293 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2294 with open(private_key_file, 'w') as o:
2295 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002296
James E. Blair498059b2016-12-20 13:50:13 -08002297 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002298 self.zk_chroot_fixture = self.useFixture(
2299 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002300 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002301 self.zk_chroot_fixture.zookeeper_host,
2302 self.zk_chroot_fixture.zookeeper_port,
2303 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002304
James E. Blair96c6bf82016-01-15 16:20:40 -08002305 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002306 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002307
2308 files = {}
2309 for (dirpath, dirnames, filenames) in os.walk(source_path):
2310 for filename in filenames:
2311 test_tree_filepath = os.path.join(dirpath, filename)
2312 common_path = os.path.commonprefix([test_tree_filepath,
2313 source_path])
2314 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2315 with open(test_tree_filepath, 'r') as f:
2316 content = f.read()
2317 files[relative_filepath] = content
2318 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002319 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002320
James E. Blaire18d4602017-01-05 11:17:28 -08002321 def assertNodepoolState(self):
2322 # Make sure that there are no pending requests
2323
2324 requests = self.fake_nodepool.getNodeRequests()
2325 self.assertEqual(len(requests), 0)
2326
2327 nodes = self.fake_nodepool.getNodes()
2328 for node in nodes:
2329 self.assertFalse(node['_lock'], "Node %s is locked" %
2330 (node['_oid'],))
2331
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002332 def assertNoGeneratedKeys(self):
2333 # Make sure that Zuul did not generate any project keys
2334 # (unless it was supposed to).
2335
2336 if self.create_project_keys:
2337 return
2338
2339 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2340 test_key = i.read()
2341
2342 key_root = os.path.join(self.state_root, 'keys')
2343 for root, dirname, files in os.walk(key_root):
2344 for fn in files:
2345 with open(os.path.join(root, fn)) as f:
2346 self.assertEqual(test_key, f.read())
2347
Clark Boylanb640e052014-04-03 16:41:46 -07002348 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002349 self.log.debug("Assert final state")
2350 # Make sure no jobs are running
2351 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002352 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002353 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002354 gc.collect()
2355 for obj in gc.get_objects():
2356 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002357 self.log.debug("Leaked git repo object: 0x%x %s" %
2358 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002359 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002360 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002361 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002362 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002363 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002364 for tenant in self.sched.abide.tenants.values():
2365 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002366 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002367 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002368
2369 def shutdown(self):
2370 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002371 self.executor_server.hold_jobs_in_build = False
2372 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002373 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002374 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002375 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002376 self.sched.stop()
2377 self.sched.join()
2378 self.statsd.stop()
2379 self.statsd.join()
2380 self.webapp.stop()
2381 self.webapp.join()
2382 self.rpc.stop()
2383 self.rpc.join()
2384 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002385 self.fake_nodepool.stop()
2386 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002387 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002388 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002389 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002390 # Further the pydevd threads also need to be whitelisted so debugging
2391 # e.g. in PyCharm is possible without breaking shutdown.
2392 whitelist = ['executor-watchdog',
2393 'pydevd.CommandThread',
2394 'pydevd.Reader',
2395 'pydevd.Writer',
2396 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002397 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002398 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002399 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002400 log_str = ""
2401 for thread_id, stack_frame in sys._current_frames().items():
2402 log_str += "Thread: %s\n" % thread_id
2403 log_str += "".join(traceback.format_stack(stack_frame))
2404 self.log.debug(log_str)
2405 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002406
James E. Blaira002b032017-04-18 10:35:48 -07002407 def assertCleanShutdown(self):
2408 pass
2409
James E. Blairc4ba97a2017-04-19 16:26:24 -07002410 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002411 parts = project.split('/')
2412 path = os.path.join(self.upstream_root, *parts[:-1])
2413 if not os.path.exists(path):
2414 os.makedirs(path)
2415 path = os.path.join(self.upstream_root, project)
2416 repo = git.Repo.init(path)
2417
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002418 with repo.config_writer() as config_writer:
2419 config_writer.set_value('user', 'email', 'user@example.com')
2420 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002421
Clark Boylanb640e052014-04-03 16:41:46 -07002422 repo.index.commit('initial commit')
2423 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002424 if tag:
2425 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002426
James E. Blair97d902e2014-08-21 13:25:56 -07002427 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002428 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002429 repo.git.clean('-x', '-f', '-d')
2430
James E. Blair97d902e2014-08-21 13:25:56 -07002431 def create_branch(self, project, branch):
2432 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002433 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002434 fn = os.path.join(path, 'README')
2435
2436 branch_head = repo.create_head(branch)
2437 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002438 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002439 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002440 f.close()
2441 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002442 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002443
James E. Blair97d902e2014-08-21 13:25:56 -07002444 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002445 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002446 repo.git.clean('-x', '-f', '-d')
2447
Sachi King9f16d522016-03-16 12:20:45 +11002448 def create_commit(self, project):
2449 path = os.path.join(self.upstream_root, project)
2450 repo = git.Repo(path)
2451 repo.head.reference = repo.heads['master']
2452 file_name = os.path.join(path, 'README')
2453 with open(file_name, 'a') as f:
2454 f.write('creating fake commit\n')
2455 repo.index.add([file_name])
2456 commit = repo.index.commit('Creating a fake commit')
2457 return commit.hexsha
2458
James E. Blairf4a5f022017-04-18 14:01:10 -07002459 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002460 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002461 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002462 while len(self.builds):
2463 self.release(self.builds[0])
2464 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002465 i += 1
2466 if count is not None and i >= count:
2467 break
James E. Blairb8c16472015-05-05 14:55:26 -07002468
James E. Blairdf25ddc2017-07-08 07:57:09 -07002469 def getSortedBuilds(self):
2470 "Return the list of currently running builds sorted by name"
2471
2472 return sorted(self.builds, key=lambda x: x.name)
2473
Clark Boylanb640e052014-04-03 16:41:46 -07002474 def release(self, job):
2475 if isinstance(job, FakeBuild):
2476 job.release()
2477 else:
2478 job.waiting = False
2479 self.log.debug("Queued job %s released" % job.unique)
2480 self.gearman_server.wakeConnections()
2481
2482 def getParameter(self, job, name):
2483 if isinstance(job, FakeBuild):
2484 return job.parameters[name]
2485 else:
2486 parameters = json.loads(job.arguments)
2487 return parameters[name]
2488
Clark Boylanb640e052014-04-03 16:41:46 -07002489 def haveAllBuildsReported(self):
2490 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002491 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002492 return False
2493 # Find out if every build that the worker has completed has been
2494 # reported back to Zuul. If it hasn't then that means a Gearman
2495 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002496 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002497 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002498 if not zbuild:
2499 # It has already been reported
2500 continue
2501 # It hasn't been reported yet.
2502 return False
2503 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002504 worker = self.executor_server.executor_worker
2505 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002506 if connection.state == 'GRAB_WAIT':
2507 return False
2508 return True
2509
2510 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002511 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002512 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002513 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002514 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002515 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002516 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002517 for j in conn.related_jobs.values():
2518 if j.unique == build.uuid:
2519 client_job = j
2520 break
2521 if not client_job:
2522 self.log.debug("%s is not known to the gearman client" %
2523 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002524 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002525 if not client_job.handle:
2526 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002527 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002528 server_job = self.gearman_server.jobs.get(client_job.handle)
2529 if not server_job:
2530 self.log.debug("%s is not known to the gearman server" %
2531 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002532 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002533 if not hasattr(server_job, 'waiting'):
2534 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002535 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002536 if server_job.waiting:
2537 continue
James E. Blair17302972016-08-10 16:11:42 -07002538 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002539 self.log.debug("%s has not reported start" % build)
2540 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002541 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002542 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002543 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002544 if worker_build:
2545 if worker_build.isWaiting():
2546 continue
2547 else:
2548 self.log.debug("%s is running" % worker_build)
2549 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002550 else:
James E. Blair962220f2016-08-03 11:22:38 -07002551 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002552 return False
James E. Blaira002b032017-04-18 10:35:48 -07002553 for (build_uuid, job_worker) in \
2554 self.executor_server.job_workers.items():
2555 if build_uuid not in seen_builds:
2556 self.log.debug("%s is not finalized" % build_uuid)
2557 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002558 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002559
James E. Blairdce6cea2016-12-20 16:45:32 -08002560 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002561 if self.fake_nodepool.paused:
2562 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002563 if self.sched.nodepool.requests:
2564 return False
2565 return True
2566
James E. Blaira615c362017-10-02 17:34:42 -07002567 def areAllMergeJobsWaiting(self):
2568 for client_job in list(self.merge_client.jobs):
2569 if not client_job.handle:
2570 self.log.debug("%s has no handle" % client_job)
2571 return False
2572 server_job = self.gearman_server.jobs.get(client_job.handle)
2573 if not server_job:
2574 self.log.debug("%s is not known to the gearman server" %
2575 client_job)
2576 return False
2577 if not hasattr(server_job, 'waiting'):
2578 self.log.debug("%s is being enqueued" % server_job)
2579 return False
2580 if server_job.waiting:
2581 self.log.debug("%s is waiting" % server_job)
2582 continue
2583 self.log.debug("%s is not waiting" % server_job)
2584 return False
2585 return True
2586
Jan Hruban6b71aff2015-10-22 16:58:08 +02002587 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002588 for event_queue in self.event_queues:
2589 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002590
2591 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002592 for event_queue in self.event_queues:
2593 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002594
Clark Boylanb640e052014-04-03 16:41:46 -07002595 def waitUntilSettled(self):
2596 self.log.debug("Waiting until settled...")
2597 start = time.time()
2598 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002599 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002600 self.log.error("Timeout waiting for Zuul to settle")
2601 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002602 for event_queue in self.event_queues:
2603 self.log.error(" %s: %s" %
2604 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002605 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002606 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002607 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002608 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002609 self.log.error("All requests completed: %s" %
2610 (self.areAllNodeRequestsComplete(),))
2611 self.log.error("Merge client jobs: %s" %
2612 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002613 raise Exception("Timeout waiting for Zuul to settle")
2614 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002615
Paul Belanger174a8272017-03-14 13:20:10 -04002616 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002617 # have all build states propogated to zuul?
2618 if self.haveAllBuildsReported():
2619 # Join ensures that the queue is empty _and_ events have been
2620 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002621 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002622 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002623 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002624 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002625 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002626 self.areAllNodeRequestsComplete() and
2627 all(self.eventQueuesEmpty())):
2628 # The queue empty check is placed at the end to
2629 # ensure that if a component adds an event between
2630 # when locked the run handler and checked that the
2631 # components were stable, we don't erroneously
2632 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002633 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002634 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002635 self.log.debug("...settled.")
2636 return
2637 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002638 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002639 self.sched.wake_event.wait(0.1)
2640
2641 def countJobResults(self, jobs, result):
2642 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002643 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002644
Monty Taylor0d926122017-05-24 08:07:56 -05002645 def getBuildByName(self, name):
2646 for build in self.builds:
2647 if build.name == name:
2648 return build
2649 raise Exception("Unable to find build %s" % name)
2650
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002651 def assertJobNotInHistory(self, name, project=None):
2652 for job in self.history:
2653 if (project is None or
2654 job.parameters['zuul']['project']['name'] == project):
2655 self.assertNotEqual(job.name, name,
2656 'Job %s found in history' % name)
2657
James E. Blair96c6bf82016-01-15 16:20:40 -08002658 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002659 for job in self.history:
2660 if (job.name == name and
2661 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002662 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002663 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002664 raise Exception("Unable to find job %s in history" % name)
2665
2666 def assertEmptyQueues(self):
2667 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002668 for tenant in self.sched.abide.tenants.values():
2669 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002670 for pipeline_queue in pipeline.queues:
2671 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002672 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002673 pipeline.name, pipeline_queue.name,
2674 pipeline_queue.queue))
2675 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002676 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002677
2678 def assertReportedStat(self, key, value=None, kind=None):
2679 start = time.time()
2680 while time.time() < (start + 5):
2681 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002682 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002683 if key == k:
2684 if value is None and kind is None:
2685 return
2686 elif value:
2687 if value == v:
2688 return
2689 elif kind:
2690 if v.endswith('|' + kind):
2691 return
2692 time.sleep(0.1)
2693
Clark Boylanb640e052014-04-03 16:41:46 -07002694 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002695
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002696 def assertBuilds(self, builds):
2697 """Assert that the running builds are as described.
2698
2699 The list of running builds is examined and must match exactly
2700 the list of builds described by the input.
2701
2702 :arg list builds: A list of dictionaries. Each item in the
2703 list must match the corresponding build in the build
2704 history, and each element of the dictionary must match the
2705 corresponding attribute of the build.
2706
2707 """
James E. Blair3158e282016-08-19 09:34:11 -07002708 try:
2709 self.assertEqual(len(self.builds), len(builds))
2710 for i, d in enumerate(builds):
2711 for k, v in d.items():
2712 self.assertEqual(
2713 getattr(self.builds[i], k), v,
2714 "Element %i in builds does not match" % (i,))
2715 except Exception:
2716 for build in self.builds:
2717 self.log.error("Running build: %s" % build)
2718 else:
2719 self.log.error("No running builds")
2720 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002721
James E. Blairb536ecc2016-08-31 10:11:42 -07002722 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002723 """Assert that the completed builds are as described.
2724
2725 The list of completed builds is examined and must match
2726 exactly the list of builds described by the input.
2727
2728 :arg list history: A list of dictionaries. Each item in the
2729 list must match the corresponding build in the build
2730 history, and each element of the dictionary must match the
2731 corresponding attribute of the build.
2732
James E. Blairb536ecc2016-08-31 10:11:42 -07002733 :arg bool ordered: If true, the history must match the order
2734 supplied, if false, the builds are permitted to have
2735 arrived in any order.
2736
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002737 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002738 def matches(history_item, item):
2739 for k, v in item.items():
2740 if getattr(history_item, k) != v:
2741 return False
2742 return True
James E. Blair3158e282016-08-19 09:34:11 -07002743 try:
2744 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002745 if ordered:
2746 for i, d in enumerate(history):
2747 if not matches(self.history[i], d):
2748 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002749 "Element %i in history does not match %s" %
2750 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002751 else:
2752 unseen = self.history[:]
2753 for i, d in enumerate(history):
2754 found = False
2755 for unseen_item in unseen:
2756 if matches(unseen_item, d):
2757 found = True
2758 unseen.remove(unseen_item)
2759 break
2760 if not found:
2761 raise Exception("No match found for element %i "
2762 "in history" % (i,))
2763 if unseen:
2764 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002765 except Exception:
2766 for build in self.history:
2767 self.log.error("Completed build: %s" % build)
2768 else:
2769 self.log.error("No completed builds")
2770 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002771
James E. Blair6ac368c2016-12-22 18:07:20 -08002772 def printHistory(self):
2773 """Log the build history.
2774
2775 This can be useful during tests to summarize what jobs have
2776 completed.
2777
2778 """
2779 self.log.debug("Build history:")
2780 for build in self.history:
2781 self.log.debug(build)
2782
James E. Blair59fdbac2015-12-07 17:08:06 -08002783 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002784 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2785
James E. Blair9ea70072017-04-19 16:05:30 -07002786 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002787 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002788 if not os.path.exists(root):
2789 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002790 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2791 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002792- tenant:
2793 name: openstack
2794 source:
2795 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002796 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002797 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002798 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002799 - org/project
2800 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002801 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002802 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002803 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002804 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002805 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002806
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002807 def addCommitToRepo(self, project, message, files,
2808 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002809 path = os.path.join(self.upstream_root, project)
2810 repo = git.Repo(path)
2811 repo.head.reference = branch
2812 zuul.merger.merger.reset_repo_to_head(repo)
2813 for fn, content in files.items():
2814 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002815 try:
2816 os.makedirs(os.path.dirname(fn))
2817 except OSError:
2818 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002819 with open(fn, 'w') as f:
2820 f.write(content)
2821 repo.index.add([fn])
2822 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002823 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002824 repo.heads[branch].commit = commit
2825 repo.head.reference = branch
2826 repo.git.clean('-x', '-f', '-d')
2827 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002828 if tag:
2829 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002830 return before
2831
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002832 def commitConfigUpdate(self, project_name, source_name):
2833 """Commit an update to zuul.yaml
2834
2835 This overwrites the zuul.yaml in the specificed project with
2836 the contents specified.
2837
2838 :arg str project_name: The name of the project containing
2839 zuul.yaml (e.g., common-config)
2840
2841 :arg str source_name: The path to the file (underneath the
2842 test fixture directory) whose contents should be used to
2843 replace zuul.yaml.
2844 """
2845
2846 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002847 files = {}
2848 with open(source_path, 'r') as f:
2849 data = f.read()
2850 layout = yaml.safe_load(data)
2851 files['zuul.yaml'] = data
2852 for item in layout:
2853 if 'job' in item:
2854 jobname = item['job']['name']
2855 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002856 before = self.addCommitToRepo(
2857 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002858 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002859 return before
2860
Clint Byrum627ba362017-08-14 13:20:40 -07002861 def newTenantConfig(self, source_name):
2862 """ Use this to update the tenant config file in tests
2863
2864 This will update self.tenant_config_file to point to a temporary file
2865 for the duration of this particular test. The content of that file will
2866 be taken from FIXTURE_DIR/source_name
2867
2868 After the test the original value of self.tenant_config_file will be
2869 restored.
2870
2871 :arg str source_name: The path of the file under
2872 FIXTURE_DIR that will be used to populate the new tenant
2873 config file.
2874 """
2875 source_path = os.path.join(FIXTURE_DIR, source_name)
2876 orig_tenant_config_file = self.tenant_config_file
2877 with tempfile.NamedTemporaryFile(
2878 delete=False, mode='wb') as new_tenant_config:
2879 self.tenant_config_file = new_tenant_config.name
2880 with open(source_path, mode='rb') as source_tenant_config:
2881 new_tenant_config.write(source_tenant_config.read())
2882 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2883 self.setupAllProjectKeys()
2884 self.log.debug(
2885 'tenant_config_file = {}'.format(self.tenant_config_file))
2886
2887 def _restoreTenantConfig():
2888 self.log.debug(
2889 'restoring tenant_config_file = {}'.format(
2890 orig_tenant_config_file))
2891 os.unlink(self.tenant_config_file)
2892 self.tenant_config_file = orig_tenant_config_file
2893 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2894 self.addCleanup(_restoreTenantConfig)
2895
James E. Blair7fc8daa2016-08-08 15:37:15 -07002896 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002897
James E. Blair7fc8daa2016-08-08 15:37:15 -07002898 """Inject a Fake (Gerrit) event.
2899
2900 This method accepts a JSON-encoded event and simulates Zuul
2901 having received it from Gerrit. It could (and should)
2902 eventually apply to any connection type, but is currently only
2903 used with Gerrit connections. The name of the connection is
2904 used to look up the corresponding server, and the event is
2905 simulated as having been received by all Zuul connections
2906 attached to that server. So if two Gerrit connections in Zuul
2907 are connected to the same Gerrit server, and you invoke this
2908 method specifying the name of one of them, the event will be
2909 received by both.
2910
2911 .. note::
2912
2913 "self.fake_gerrit.addEvent" calls should be migrated to
2914 this method.
2915
2916 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002917 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002918 :arg str event: The JSON-encoded event.
2919
2920 """
2921 specified_conn = self.connections.connections[connection]
2922 for conn in self.connections.connections.values():
2923 if (isinstance(conn, specified_conn.__class__) and
2924 specified_conn.server == conn.server):
2925 conn.addEvent(event)
2926
James E. Blaird8af5422017-05-24 13:59:40 -07002927 def getUpstreamRepos(self, projects):
2928 """Return upstream git repo objects for the listed projects
2929
2930 :arg list projects: A list of strings, each the canonical name
2931 of a project.
2932
2933 :returns: A dictionary of {name: repo} for every listed
2934 project.
2935 :rtype: dict
2936
2937 """
2938
2939 repos = {}
2940 for project in projects:
2941 # FIXME(jeblair): the upstream root does not yet have a
2942 # hostname component; that needs to be added, and this
2943 # line removed:
2944 tmp_project_name = '/'.join(project.split('/')[1:])
2945 path = os.path.join(self.upstream_root, tmp_project_name)
2946 repo = git.Repo(path)
2947 repos[project] = repo
2948 return repos
2949
James E. Blair3f876d52016-07-22 13:07:14 -07002950
2951class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002952 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002953 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002954
Jamie Lennox7655b552017-03-17 12:33:38 +11002955 @contextmanager
2956 def jobLog(self, build):
2957 """Print job logs on assertion errors
2958
2959 This method is a context manager which, if it encounters an
2960 ecxeption, adds the build log to the debug output.
2961
2962 :arg Build build: The build that's being asserted.
2963 """
2964 try:
2965 yield
2966 except Exception:
2967 path = os.path.join(self.test_root, build.uuid,
2968 'work', 'logs', 'job-output.txt')
2969 with open(path) as f:
2970 self.log.debug(f.read())
2971 raise
2972
Joshua Heskethd78b4482015-09-14 16:56:34 -06002973
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002974class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002975 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002976 use_ssl = True
2977
2978
Joshua Heskethd78b4482015-09-14 16:56:34 -06002979class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002980 def setup_config(self):
2981 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002982 for section_name in self.config.sections():
2983 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2984 section_name, re.I)
2985 if not con_match:
2986 continue
2987
2988 if self.config.get(section_name, 'driver') == 'sql':
2989 f = MySQLSchemaFixture()
2990 self.useFixture(f)
2991 if (self.config.get(section_name, 'dburi') ==
2992 '$MYSQL_FIXTURE_DBURI$'):
2993 self.config.set(section_name, 'dburi', f.dburi)