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