blob: 2e3d6821416893878a5f51f20c1b4939dec885f9 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Jamie Lennox7655b552017-03-17 12:33:38 +110019from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080020import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070021import gc
22import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050023import importlib
24from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070025import json
26import logging
27import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050028import queue
Clark Boylanb640e052014-04-03 16:41:46 -070029import random
30import re
31import select
32import shutil
33import socket
34import string
35import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
Clark Boylan8208c192017-04-24 18:08:08 -070039import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070040import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060041import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050042import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060043
Clark Boylanb640e052014-04-03 16:41:46 -070044
45import git
46import gear
47import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080048import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080049import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060050import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070051import statsd
52import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080053import testtools.content
54import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080055from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000056import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070057
James E. Blaire511d2f2016-12-08 15:22:26 -080058import zuul.driver.gerrit.gerritsource as gerritsource
59import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070060import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.scheduler
62import zuul.webapp
63import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040064import zuul.executor.server
65import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080066import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070067import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070068import zuul.merger.merger
69import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020070import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200137 categories = {'Approved': ('Approved', -1, 1),
138 'Code-Review': ('Code-Review', -2, 2),
139 'Verified': ('Verified', -2, 2)}
140
Clark Boylanb640e052014-04-03 16:41:46 -0700141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700142 status='NEW', upstream_root=None, files={},
143 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700145 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.reported = 0
147 self.queried = 0
148 self.patchsets = []
149 self.number = number
150 self.project = project
151 self.branch = branch
152 self.subject = subject
153 self.latest_patchset = 0
154 self.depends_on_change = None
155 self.needed_by_changes = []
156 self.fail_merge = False
157 self.messages = []
158 self.data = {
159 'branch': branch,
160 'comments': [],
161 'commitMessage': subject,
162 'createdOn': time.time(),
163 'id': 'I' + random_sha1(),
164 'lastUpdated': time.time(),
165 'number': str(number),
166 'open': status == 'NEW',
167 'owner': {'email': 'user@example.com',
168 'name': 'User Name',
169 'username': 'username'},
170 'patchSets': self.patchsets,
171 'project': project,
172 'status': status,
173 'subject': subject,
174 'submitRecords': [],
175 'url': 'https://hostname/%s' % number}
176
177 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700178 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 self.data['submitRecords'] = self.getSubmitRecords()
180 self.open = status == 'NEW'
181
James E. Blair289f5932017-07-27 15:02:29 -0700182 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 path = os.path.join(self.upstream_root, self.project)
184 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700185 if parent is None:
186 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700187 ref = GerritChangeReference.create(
188 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700189 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700191 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700192 repo.git.clean('-x', '-f', '-d')
193
194 path = os.path.join(self.upstream_root, self.project)
195 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700196 for fn, content in files.items():
197 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700198 if content is None:
199 os.unlink(fn)
200 repo.index.remove([fn])
201 else:
202 d = os.path.dirname(fn)
203 if not os.path.exists(d):
204 os.makedirs(d)
205 with open(fn, 'w') as f:
206 f.write(content)
207 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700208 else:
209 for fni in range(100):
210 fn = os.path.join(path, str(fni))
211 f = open(fn, 'w')
212 for ci in range(4096):
213 f.write(random.choice(string.printable))
214 f.close()
215 repo.index.add([fn])
216
217 r = repo.index.commit(msg)
218 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700219 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700220 repo.git.clean('-x', '-f', '-d')
221 repo.heads['master'].checkout()
222 return r
223
James E. Blair289f5932017-07-27 15:02:29 -0700224 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700225 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700227 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700228 data = ("test %s %s %s\n" %
229 (self.branch, self.number, self.latest_patchset))
230 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700231 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700232 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700233 ps_files = [{'file': '/COMMIT_MSG',
234 'type': 'ADDED'},
235 {'file': 'README',
236 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700237 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700238 ps_files.append({'file': f, 'type': 'ADDED'})
239 d = {'approvals': [],
240 'createdOn': time.time(),
241 'files': ps_files,
242 'number': str(self.latest_patchset),
243 'ref': 'refs/changes/1/%s/%s' % (self.number,
244 self.latest_patchset),
245 'revision': c.hexsha,
246 'uploader': {'email': 'user@example.com',
247 'name': 'User name',
248 'username': 'user'}}
249 self.data['currentPatchSet'] = d
250 self.patchsets.append(d)
251 self.data['submitRecords'] = self.getSubmitRecords()
252
253 def getPatchsetCreatedEvent(self, patchset):
254 event = {"type": "patchset-created",
255 "change": {"project": self.project,
256 "branch": self.branch,
257 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
258 "number": str(self.number),
259 "subject": self.subject,
260 "owner": {"name": "User Name"},
261 "url": "https://hostname/3"},
262 "patchSet": self.patchsets[patchset - 1],
263 "uploader": {"name": "User Name"}}
264 return event
265
266 def getChangeRestoredEvent(self):
267 event = {"type": "change-restored",
268 "change": {"project": self.project,
269 "branch": self.branch,
270 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
271 "number": str(self.number),
272 "subject": self.subject,
273 "owner": {"name": "User Name"},
274 "url": "https://hostname/3"},
275 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100276 "patchSet": self.patchsets[-1],
277 "reason": ""}
278 return event
279
280 def getChangeAbandonedEvent(self):
281 event = {"type": "change-abandoned",
282 "change": {"project": self.project,
283 "branch": self.branch,
284 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
285 "number": str(self.number),
286 "subject": self.subject,
287 "owner": {"name": "User Name"},
288 "url": "https://hostname/3"},
289 "abandoner": {"name": "User Name"},
290 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700291 "reason": ""}
292 return event
293
294 def getChangeCommentEvent(self, patchset):
295 event = {"type": "comment-added",
296 "change": {"project": self.project,
297 "branch": self.branch,
298 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
299 "number": str(self.number),
300 "subject": self.subject,
301 "owner": {"name": "User Name"},
302 "url": "https://hostname/3"},
303 "patchSet": self.patchsets[patchset - 1],
304 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200305 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700306 "description": "Code-Review",
307 "value": "0"}],
308 "comment": "This is a comment"}
309 return event
310
James E. Blairc2a5ed72017-02-20 14:12:01 -0500311 def getChangeMergedEvent(self):
312 event = {"submitter": {"name": "Jenkins",
313 "username": "jenkins"},
314 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
315 "patchSet": self.patchsets[-1],
316 "change": self.data,
317 "type": "change-merged",
318 "eventCreatedOn": 1487613810}
319 return event
320
James E. Blair8cce42e2016-10-18 08:18:36 -0700321 def getRefUpdatedEvent(self):
322 path = os.path.join(self.upstream_root, self.project)
323 repo = git.Repo(path)
324 oldrev = repo.heads[self.branch].commit.hexsha
325
326 event = {
327 "type": "ref-updated",
328 "submitter": {
329 "name": "User Name",
330 },
331 "refUpdate": {
332 "oldRev": oldrev,
333 "newRev": self.patchsets[-1]['revision'],
334 "refName": self.branch,
335 "project": self.project,
336 }
337 }
338 return event
339
Joshua Hesketh642824b2014-07-01 17:54:59 +1000340 def addApproval(self, category, value, username='reviewer_john',
341 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700342 if not granted_on:
343 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000344 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200345 'description': self.categories[category][0],
346 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000347 'value': str(value),
348 'by': {
349 'username': username,
350 'email': username + '@example.com',
351 },
352 'grantedOn': int(granted_on)
353 }
Clark Boylanb640e052014-04-03 16:41:46 -0700354 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200355 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700356 del self.patchsets[-1]['approvals'][i]
357 self.patchsets[-1]['approvals'].append(approval)
358 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000359 'author': {'email': 'author@example.com',
360 'name': 'Patchset Author',
361 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'change': {'branch': self.branch,
363 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
364 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000365 'owner': {'email': 'owner@example.com',
366 'name': 'Change Owner',
367 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700368 'project': self.project,
369 'subject': self.subject,
370 'topic': 'master',
371 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000372 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700373 'patchSet': self.patchsets[-1],
374 'type': 'comment-added'}
375 self.data['submitRecords'] = self.getSubmitRecords()
376 return json.loads(json.dumps(event))
377
378 def getSubmitRecords(self):
379 status = {}
380 for cat in self.categories.keys():
381 status[cat] = 0
382
383 for a in self.patchsets[-1]['approvals']:
384 cur = status[a['type']]
385 cat_min, cat_max = self.categories[a['type']][1:]
386 new = int(a['value'])
387 if new == cat_min:
388 cur = new
389 elif abs(new) > abs(cur):
390 cur = new
391 status[a['type']] = cur
392
393 labels = []
394 ok = True
395 for typ, cat in self.categories.items():
396 cur = status[typ]
397 cat_min, cat_max = cat[1:]
398 if cur == cat_min:
399 value = 'REJECT'
400 ok = False
401 elif cur == cat_max:
402 value = 'OK'
403 else:
404 value = 'NEED'
405 ok = False
406 labels.append({'label': cat[0], 'status': value})
407 if ok:
408 return [{'status': 'OK'}]
409 return [{'status': 'NOT_READY',
410 'labels': labels}]
411
412 def setDependsOn(self, other, patchset):
413 self.depends_on_change = other
414 d = {'id': other.data['id'],
415 'number': other.data['number'],
416 'ref': other.patchsets[patchset - 1]['ref']
417 }
418 self.data['dependsOn'] = [d]
419
420 other.needed_by_changes.append(self)
421 needed = other.data.get('neededBy', [])
422 d = {'id': self.data['id'],
423 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700424 'ref': self.patchsets[-1]['ref'],
425 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700426 }
427 needed.append(d)
428 other.data['neededBy'] = needed
429
430 def query(self):
431 self.queried += 1
432 d = self.data.get('dependsOn')
433 if d:
434 d = d[0]
435 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
436 d['isCurrentPatchSet'] = True
437 else:
438 d['isCurrentPatchSet'] = False
439 return json.loads(json.dumps(self.data))
440
441 def setMerged(self):
442 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000443 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700444 return
445 if self.fail_merge:
446 return
447 self.data['status'] = 'MERGED'
448 self.open = False
449
450 path = os.path.join(self.upstream_root, self.project)
451 repo = git.Repo(path)
452 repo.heads[self.branch].commit = \
453 repo.commit(self.patchsets[-1]['revision'])
454
455 def setReported(self):
456 self.reported += 1
457
458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700460 """A Fake Gerrit connection for use in tests.
461
462 This subclasses
463 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
464 ability for tests to add changes to the fake Gerrit it represents.
465 """
466
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700468
James E. Blaire511d2f2016-12-08 15:22:26 -0800469 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700470 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800471 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000472 connection_config)
473
Monty Taylorb934c1a2017-06-16 19:31:47 -0500474 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700475 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
476 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700478 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200479 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700480
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700481 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700482 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700483 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700484 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700485 c = FakeGerritChange(self, self.change_number, project, branch,
486 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700487 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700488 self.changes[self.change_number] = c
489 return c
490
James E. Blair72facdc2017-08-17 10:29:12 -0700491 def getFakeBranchCreatedEvent(self, project, branch):
492 path = os.path.join(self.upstream_root, project)
493 repo = git.Repo(path)
494 oldrev = 40 * '0'
495
496 event = {
497 "type": "ref-updated",
498 "submitter": {
499 "name": "User Name",
500 },
501 "refUpdate": {
502 "oldRev": oldrev,
503 "newRev": repo.heads[branch].commit.hexsha,
504 "refName": branch,
505 "project": project,
506 }
507 }
508 return event
509
Clark Boylanb640e052014-04-03 16:41:46 -0700510 def review(self, project, changeid, message, action):
511 number, ps = changeid.split(',')
512 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000513
514 # Add the approval back onto the change (ie simulate what gerrit would
515 # do).
516 # Usually when zuul leaves a review it'll create a feedback loop where
517 # zuul's review enters another gerrit event (which is then picked up by
518 # zuul). However, we can't mimic this behaviour (by adding this
519 # approval event into the queue) as it stops jobs from checking what
520 # happens before this event is triggered. If a job needs to see what
521 # happens they can add their own verified event into the queue.
522 # Nevertheless, we can update change with the new review in gerrit.
523
James E. Blair8b5408c2016-08-08 15:37:46 -0700524 for cat in action.keys():
525 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000526 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000527
Clark Boylanb640e052014-04-03 16:41:46 -0700528 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000529
Clark Boylanb640e052014-04-03 16:41:46 -0700530 if 'submit' in action:
531 change.setMerged()
532 if message:
533 change.setReported()
534
535 def query(self, number):
536 change = self.changes.get(int(number))
537 if change:
538 return change.query()
539 return {}
540
James E. Blairc494d542014-08-06 09:23:52 -0700541 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700542 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700543 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800544 if query.startswith('change:'):
545 # Query a specific changeid
546 changeid = query[len('change:'):]
547 l = [change.query() for change in self.changes.values()
548 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700549 elif query.startswith('message:'):
550 # Query the content of a commit message
551 msg = query[len('message:'):].strip()
552 l = [change.query() for change in self.changes.values()
553 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800554 else:
555 # Query all open changes
556 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700557 return l
James E. Blairc494d542014-08-06 09:23:52 -0700558
Joshua Hesketh352264b2015-08-11 23:42:08 +1000559 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700560 pass
561
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200562 def _uploadPack(self, project):
563 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
564 'multi_ack thin-pack side-band side-band-64k ofs-delta '
565 'shallow no-progress include-tag multi_ack_detailed no-done\n')
566 path = os.path.join(self.upstream_root, project.name)
567 repo = git.Repo(path)
568 for ref in repo.refs:
569 r = ref.object.hexsha + ' ' + ref.path + '\n'
570 ret += '%04x%s' % (len(r) + 4, r)
571 ret += '0000'
572 return ret
573
Joshua Hesketh352264b2015-08-11 23:42:08 +1000574 def getGitUrl(self, project):
575 return os.path.join(self.upstream_root, project.name)
576
Clark Boylanb640e052014-04-03 16:41:46 -0700577
Gregory Haynes4fc12542015-04-22 20:38:06 -0700578class GithubChangeReference(git.Reference):
579 _common_path_default = "refs/pull"
580 _points_to_commits_only = True
581
582
Tobias Henkel64e37a02017-08-02 10:13:30 +0200583class FakeGithub(object):
584
585 class FakeUser(object):
586 def __init__(self, login):
587 self.login = login
588 self.name = "Github User"
589 self.email = "github.user@example.com"
590
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200591 class FakeBranch(object):
592 def __init__(self, branch='master'):
593 self.name = branch
594
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200595 class FakeStatus(object):
596 def __init__(self, state, url, description, context, user):
597 self._state = state
598 self._url = url
599 self._description = description
600 self._context = context
601 self._user = user
602
603 def as_dict(self):
604 return {
605 'state': self._state,
606 'url': self._url,
607 'description': self._description,
608 'context': self._context,
609 'creator': {
610 'login': self._user
611 }
612 }
613
614 class FakeCommit(object):
615 def __init__(self):
616 self._statuses = []
617
618 def set_status(self, state, url, description, context, user):
619 status = FakeGithub.FakeStatus(
620 state, url, description, context, user)
621 # always insert a status to the front of the list, to represent
622 # the last status provided for a commit.
623 self._statuses.insert(0, status)
624
625 def statuses(self):
626 return self._statuses
627
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200628 class FakeRepository(object):
629 def __init__(self):
630 self._branches = [FakeGithub.FakeBranch()]
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200631 self._commits = {}
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200632
Tobias Henkeleca46202017-08-02 20:27:10 +0200633 def branches(self, protected=False):
634 if protected:
635 # simulate there is no protected branch
636 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200637 return self._branches
638
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200639 def create_status(self, sha, state, url, description, context,
640 user='zuul'):
641 # Since we're bypassing github API, which would require a user, we
642 # default the user as 'zuul' here.
643 commit = self._commits.get(sha, None)
644 if commit is None:
645 commit = FakeGithub.FakeCommit()
646 self._commits[sha] = commit
647 commit.set_status(state, url, description, context, user)
648
649 def commit(self, sha):
650 commit = self._commits.get(sha, None)
651 if commit is None:
652 commit = FakeGithub.FakeCommit()
653 self._commits[sha] = commit
654 return commit
655
656 def __init__(self):
657 self._repos = {}
658
Tobias Henkel64e37a02017-08-02 10:13:30 +0200659 def user(self, login):
660 return self.FakeUser(login)
661
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200662 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200663 return self._repos.get((owner, proj), None)
664
665 def repo_from_project(self, project):
666 # This is a convenience method for the tests.
667 owner, proj = project.split('/')
668 return self.repository(owner, proj)
669
670 def addProject(self, project):
671 owner, proj = project.name.split('/')
672 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200673
Tobias Henkel64e37a02017-08-02 10:13:30 +0200674
Gregory Haynes4fc12542015-04-22 20:38:06 -0700675class FakeGithubPullRequest(object):
676
677 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800678 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700679 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700680 """Creates a new PR with several commits.
681 Sends an event about opened PR."""
682 self.github = github
683 self.source = github
684 self.number = number
685 self.project = project
686 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100687 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700688 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100689 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700690 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100691 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700692 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100693 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100694 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800695 self.reviews = []
696 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700697 self.updated_at = None
698 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100699 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100700 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700701 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700702 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100703 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700704 self._updateTimeStamp()
705
Jan Hruban570d01c2016-03-10 21:51:32 +0100706 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700707 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100708 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700709 self._updateTimeStamp()
710
Jan Hruban570d01c2016-03-10 21:51:32 +0100711 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700712 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100713 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700714 self._updateTimeStamp()
715
716 def getPullRequestOpenedEvent(self):
717 return self._getPullRequestEvent('opened')
718
719 def getPullRequestSynchronizeEvent(self):
720 return self._getPullRequestEvent('synchronize')
721
722 def getPullRequestReopenedEvent(self):
723 return self._getPullRequestEvent('reopened')
724
725 def getPullRequestClosedEvent(self):
726 return self._getPullRequestEvent('closed')
727
Jesse Keatinga41566f2017-06-14 18:17:51 -0700728 def getPullRequestEditedEvent(self):
729 return self._getPullRequestEvent('edited')
730
Gregory Haynes4fc12542015-04-22 20:38:06 -0700731 def addComment(self, message):
732 self.comments.append(message)
733 self._updateTimeStamp()
734
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200735 def getCommentAddedEvent(self, text):
736 name = 'issue_comment'
737 data = {
738 'action': 'created',
739 'issue': {
740 'number': self.number
741 },
742 'comment': {
743 'body': text
744 },
745 'repository': {
746 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100747 },
748 'sender': {
749 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200750 }
751 }
752 return (name, data)
753
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800754 def getReviewAddedEvent(self, review):
755 name = 'pull_request_review'
756 data = {
757 'action': 'submitted',
758 'pull_request': {
759 'number': self.number,
760 'title': self.subject,
761 'updated_at': self.updated_at,
762 'base': {
763 'ref': self.branch,
764 'repo': {
765 'full_name': self.project
766 }
767 },
768 'head': {
769 'sha': self.head_sha
770 }
771 },
772 'review': {
773 'state': review
774 },
775 'repository': {
776 'full_name': self.project
777 },
778 'sender': {
779 'login': 'ghuser'
780 }
781 }
782 return (name, data)
783
Jan Hruban16ad31f2015-11-07 14:39:07 +0100784 def addLabel(self, name):
785 if name not in self.labels:
786 self.labels.append(name)
787 self._updateTimeStamp()
788 return self._getLabelEvent(name)
789
790 def removeLabel(self, name):
791 if name in self.labels:
792 self.labels.remove(name)
793 self._updateTimeStamp()
794 return self._getUnlabelEvent(name)
795
796 def _getLabelEvent(self, label):
797 name = 'pull_request'
798 data = {
799 'action': 'labeled',
800 'pull_request': {
801 'number': self.number,
802 'updated_at': self.updated_at,
803 'base': {
804 'ref': self.branch,
805 'repo': {
806 'full_name': self.project
807 }
808 },
809 'head': {
810 'sha': self.head_sha
811 }
812 },
813 'label': {
814 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100815 },
816 'sender': {
817 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100818 }
819 }
820 return (name, data)
821
822 def _getUnlabelEvent(self, label):
823 name = 'pull_request'
824 data = {
825 'action': 'unlabeled',
826 'pull_request': {
827 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100828 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100829 'updated_at': self.updated_at,
830 'base': {
831 'ref': self.branch,
832 'repo': {
833 'full_name': self.project
834 }
835 },
836 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800837 'sha': self.head_sha,
838 'repo': {
839 'full_name': self.project
840 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100841 }
842 },
843 'label': {
844 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100845 },
846 'sender': {
847 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100848 }
849 }
850 return (name, data)
851
Jesse Keatinga41566f2017-06-14 18:17:51 -0700852 def editBody(self, body):
853 self.body = body
854 self._updateTimeStamp()
855
Gregory Haynes4fc12542015-04-22 20:38:06 -0700856 def _getRepo(self):
857 repo_path = os.path.join(self.upstream_root, self.project)
858 return git.Repo(repo_path)
859
860 def _createPRRef(self):
861 repo = self._getRepo()
862 GithubChangeReference.create(
863 repo, self._getPRReference(), 'refs/tags/init')
864
Jan Hruban570d01c2016-03-10 21:51:32 +0100865 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700866 repo = self._getRepo()
867 ref = repo.references[self._getPRReference()]
868 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100869 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700870 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100871 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700872 repo.head.reference = ref
873 zuul.merger.merger.reset_repo_to_head(repo)
874 repo.git.clean('-x', '-f', '-d')
875
Jan Hruban570d01c2016-03-10 21:51:32 +0100876 if files:
877 fn = files[0]
878 self.files = files
879 else:
880 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
881 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100882 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700883 fn = os.path.join(repo.working_dir, fn)
884 f = open(fn, 'w')
885 with open(fn, 'w') as f:
886 f.write("test %s %s\n" %
887 (self.branch, self.number))
888 repo.index.add([fn])
889
890 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800891 # Create an empty set of statuses for the given sha,
892 # each sha on a PR may have a status set on it
893 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700894 repo.head.reference = 'master'
895 zuul.merger.merger.reset_repo_to_head(repo)
896 repo.git.clean('-x', '-f', '-d')
897 repo.heads['master'].checkout()
898
899 def _updateTimeStamp(self):
900 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
901
902 def getPRHeadSha(self):
903 repo = self._getRepo()
904 return repo.references[self._getPRReference()].commit.hexsha
905
Jesse Keatingae4cd272017-01-30 17:10:44 -0800906 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800907 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
908 # convert the timestamp to a str format that would be returned
909 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800910
Adam Gandelmand81dd762017-02-09 15:15:49 -0800911 if granted_on:
912 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
913 submitted_at = time.strftime(
914 gh_time_format, granted_on.timetuple())
915 else:
916 # github timestamps only down to the second, so we need to make
917 # sure reviews that tests add appear to be added over a period of
918 # time in the past and not all at once.
919 if not self.reviews:
920 # the first review happens 10 mins ago
921 offset = 600
922 else:
923 # subsequent reviews happen 1 minute closer to now
924 offset = 600 - (len(self.reviews) * 60)
925
926 granted_on = datetime.datetime.utcfromtimestamp(
927 time.time() - offset)
928 submitted_at = time.strftime(
929 gh_time_format, granted_on.timetuple())
930
Jesse Keatingae4cd272017-01-30 17:10:44 -0800931 self.reviews.append({
932 'state': state,
933 'user': {
934 'login': user,
935 'email': user + "@derp.com",
936 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800937 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800938 })
939
Gregory Haynes4fc12542015-04-22 20:38:06 -0700940 def _getPRReference(self):
941 return '%s/head' % self.number
942
943 def _getPullRequestEvent(self, action):
944 name = 'pull_request'
945 data = {
946 'action': action,
947 'number': self.number,
948 'pull_request': {
949 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100950 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700951 'updated_at': self.updated_at,
952 'base': {
953 'ref': self.branch,
954 'repo': {
955 'full_name': self.project
956 }
957 },
958 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800959 'sha': self.head_sha,
960 'repo': {
961 'full_name': self.project
962 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700963 },
964 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100965 },
966 'sender': {
967 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 }
969 }
970 return (name, data)
971
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800972 def getCommitStatusEvent(self, context, state='success', user='zuul'):
973 name = 'status'
974 data = {
975 'state': state,
976 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700977 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800978 'description': 'Test results for %s: %s' % (self.head_sha, state),
979 'target_url': 'http://zuul/%s' % self.head_sha,
980 'branches': [],
981 'context': context,
982 'sender': {
983 'login': user
984 }
985 }
986 return (name, data)
987
James E. Blair289f5932017-07-27 15:02:29 -0700988 def setMerged(self, commit_message):
989 self.is_merged = True
990 self.merge_message = commit_message
991
992 repo = self._getRepo()
993 repo.heads[self.branch].commit = repo.commit(self.head_sha)
994
Gregory Haynes4fc12542015-04-22 20:38:06 -0700995
996class FakeGithubConnection(githubconnection.GithubConnection):
997 log = logging.getLogger("zuul.test.FakeGithubConnection")
998
999 def __init__(self, driver, connection_name, connection_config,
1000 upstream_root=None):
1001 super(FakeGithubConnection, self).__init__(driver, connection_name,
1002 connection_config)
1003 self.connection_name = connection_name
1004 self.pr_number = 0
1005 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001006 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001007 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001008 self.merge_failure = False
1009 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001010 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +02001011 self.github_client = FakeGithub()
1012
1013 def getGithubClient(self,
1014 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -06001015 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +02001016 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001017
Jesse Keatinga41566f2017-06-14 18:17:51 -07001018 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001019 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001020 self.pr_number += 1
1021 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001022 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001023 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001024 self.pull_requests.append(pull_request)
1025 return pull_request
1026
Jesse Keating71a47ff2017-06-06 11:36:43 -07001027 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1028 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001029 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001030 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001031 if not new_rev:
1032 new_rev = random_sha1()
1033 name = 'push'
1034 data = {
1035 'ref': ref,
1036 'before': old_rev,
1037 'after': new_rev,
1038 'repository': {
1039 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001040 },
1041 'commits': [
1042 {
1043 'added': added_files,
1044 'removed': removed_files,
1045 'modified': modified_files
1046 }
1047 ]
Wayne1a78c612015-06-11 17:14:13 -07001048 }
1049 return (name, data)
1050
Gregory Haynes4fc12542015-04-22 20:38:06 -07001051 def emitEvent(self, event):
1052 """Emulates sending the GitHub webhook event to the connection."""
1053 port = self.webapp.server.socket.getsockname()[1]
1054 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001055 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001056 secret = self.connection_config['webhook_token']
1057 signature = githubconnection._sign_request(payload, secret)
1058 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001059 req = urllib.request.Request(
1060 'http://localhost:%s/connection/%s/payload'
1061 % (port, self.connection_name),
1062 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001063 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001064
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001065 def addProject(self, project):
1066 # use the original method here and additionally register it in the
1067 # fake github
1068 super(FakeGithubConnection, self).addProject(project)
1069 self.getGithubClient(project).addProject(project)
1070
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001071 def getPull(self, project, number):
1072 pr = self.pull_requests[number - 1]
1073 data = {
1074 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001075 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001076 'updated_at': pr.updated_at,
1077 'base': {
1078 'repo': {
1079 'full_name': pr.project
1080 },
1081 'ref': pr.branch,
1082 },
Jan Hruban37615e52015-11-19 14:30:49 +01001083 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001084 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001085 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001086 'sha': pr.head_sha,
1087 'repo': {
1088 'full_name': pr.project
1089 }
Jesse Keating61040e72017-06-08 15:08:27 -07001090 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001091 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001092 'labels': pr.labels,
1093 'merged': pr.is_merged,
1094 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001095 }
1096 return data
1097
Jesse Keating9021a012017-08-29 14:45:27 -07001098 def getPullBySha(self, sha, project):
1099 prs = list(set([p for p in self.pull_requests if
1100 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001101 if len(prs) > 1:
1102 raise Exception('Multiple pulls found with head sha: %s' % sha)
1103 pr = prs[0]
1104 return self.getPull(pr.project, pr.number)
1105
Jesse Keatingae4cd272017-01-30 17:10:44 -08001106 def _getPullReviews(self, owner, project, number):
1107 pr = self.pull_requests[number - 1]
1108 return pr.reviews
1109
Jesse Keatingae4cd272017-01-30 17:10:44 -08001110 def getRepoPermission(self, project, login):
1111 owner, proj = project.split('/')
1112 for pr in self.pull_requests:
1113 pr_owner, pr_project = pr.project.split('/')
1114 if (pr_owner == owner and proj == pr_project):
1115 if login in pr.writers:
1116 return 'write'
1117 else:
1118 return 'read'
1119
Gregory Haynes4fc12542015-04-22 20:38:06 -07001120 def getGitUrl(self, project):
1121 return os.path.join(self.upstream_root, str(project))
1122
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001123 def real_getGitUrl(self, project):
1124 return super(FakeGithubConnection, self).getGitUrl(project)
1125
Jan Hrubane252a732017-01-03 15:03:09 +01001126 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001127 # record that this got reported
1128 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001129 pull_request = self.pull_requests[pr_number - 1]
1130 pull_request.addComment(message)
1131
Jan Hruban3b415922016-02-03 13:10:22 +01001132 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001133 # record that this got reported
1134 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001135 pull_request = self.pull_requests[pr_number - 1]
1136 if self.merge_failure:
1137 raise Exception('Pull request was not merged')
1138 if self.merge_not_allowed_count > 0:
1139 self.merge_not_allowed_count -= 1
1140 raise MergeFailure('Merge was not successful due to mergeability'
1141 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001142 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001143
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001144 def setCommitStatus(self, project, sha, state, url='', description='',
1145 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001146 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001147 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001148 super(FakeGithubConnection, self).setCommitStatus(
1149 project, sha, state,
1150 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001151
Jan Hruban16ad31f2015-11-07 14:39:07 +01001152 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001153 # record that this got reported
1154 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001155 pull_request = self.pull_requests[pr_number - 1]
1156 pull_request.addLabel(label)
1157
1158 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001159 # record that this got reported
1160 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001161 pull_request = self.pull_requests[pr_number - 1]
1162 pull_request.removeLabel(label)
1163
Jesse Keatinga41566f2017-06-14 18:17:51 -07001164 def _getNeededByFromPR(self, change):
1165 prs = []
1166 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001167 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001168 change.number))
1169 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001170 if not pr.body:
1171 body = ''
1172 else:
1173 body = pr.body
1174 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001175 # Get our version of a pull so that it's a dict
1176 pull = self.getPull(pr.project, pr.number)
1177 prs.append(pull)
1178
1179 return prs
1180
Gregory Haynes4fc12542015-04-22 20:38:06 -07001181
Clark Boylanb640e052014-04-03 16:41:46 -07001182class BuildHistory(object):
1183 def __init__(self, **kw):
1184 self.__dict__.update(kw)
1185
1186 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001187 return ("<Completed build, result: %s name: %s uuid: %s "
1188 "changes: %s ref: %s>" %
1189 (self.result, self.name, self.uuid,
1190 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001191
1192
Clark Boylanb640e052014-04-03 16:41:46 -07001193class FakeStatsd(threading.Thread):
1194 def __init__(self):
1195 threading.Thread.__init__(self)
1196 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001197 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001198 self.sock.bind(('', 0))
1199 self.port = self.sock.getsockname()[1]
1200 self.wake_read, self.wake_write = os.pipe()
1201 self.stats = []
1202
1203 def run(self):
1204 while True:
1205 poll = select.poll()
1206 poll.register(self.sock, select.POLLIN)
1207 poll.register(self.wake_read, select.POLLIN)
1208 ret = poll.poll()
1209 for (fd, event) in ret:
1210 if fd == self.sock.fileno():
1211 data = self.sock.recvfrom(1024)
1212 if not data:
1213 return
1214 self.stats.append(data[0])
1215 if fd == self.wake_read:
1216 return
1217
1218 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001219 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001220
1221
James E. Blaire1767bc2016-08-02 10:00:27 -07001222class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001223 log = logging.getLogger("zuul.test")
1224
Paul Belanger174a8272017-03-14 13:20:10 -04001225 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001226 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001227 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001228 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001229 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001230 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001231 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001232 # TODOv3(jeblair): self.node is really "the label of the node
1233 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001234 # keep using it like this, or we may end up exposing more of
1235 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001236 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001237 self.node = None
1238 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001239 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001240 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001241 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001242 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001243 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001244 self.wait_condition = threading.Condition()
1245 self.waiting = False
1246 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001247 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001248 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001249 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001250 items = self.parameters['zuul']['items']
1251 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1252 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001253
James E. Blair3158e282016-08-19 09:34:11 -07001254 def __repr__(self):
1255 waiting = ''
1256 if self.waiting:
1257 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001258 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1259 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001260
Clark Boylanb640e052014-04-03 16:41:46 -07001261 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001262 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001263 self.wait_condition.acquire()
1264 self.wait_condition.notify()
1265 self.waiting = False
1266 self.log.debug("Build %s released" % self.unique)
1267 self.wait_condition.release()
1268
1269 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001270 """Return whether this build is being held.
1271
1272 :returns: Whether the build is being held.
1273 :rtype: bool
1274 """
1275
Clark Boylanb640e052014-04-03 16:41:46 -07001276 self.wait_condition.acquire()
1277 if self.waiting:
1278 ret = True
1279 else:
1280 ret = False
1281 self.wait_condition.release()
1282 return ret
1283
1284 def _wait(self):
1285 self.wait_condition.acquire()
1286 self.waiting = True
1287 self.log.debug("Build %s waiting" % self.unique)
1288 self.wait_condition.wait()
1289 self.wait_condition.release()
1290
1291 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001292 self.log.debug('Running build %s' % self.unique)
1293
Paul Belanger174a8272017-03-14 13:20:10 -04001294 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001295 self.log.debug('Holding build %s' % self.unique)
1296 self._wait()
1297 self.log.debug("Build %s continuing" % self.unique)
1298
James E. Blair412fba82017-01-26 15:00:50 -08001299 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001300 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001301 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001302 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001303 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001304 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001305 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001306
James E. Blaire1767bc2016-08-02 10:00:27 -07001307 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001308
James E. Blaira5dba232016-08-08 15:53:24 -07001309 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001310 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001311 for change in changes:
1312 if self.hasChanges(change):
1313 return True
1314 return False
1315
James E. Blaire7b99a02016-08-05 14:27:34 -07001316 def hasChanges(self, *changes):
1317 """Return whether this build has certain changes in its git repos.
1318
1319 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001320 are expected to be present (in order) in the git repository of
1321 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001322
1323 :returns: Whether the build has the indicated changes.
1324 :rtype: bool
1325
1326 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001327 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001328 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001329 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001330 try:
1331 repo = git.Repo(path)
1332 except NoSuchPathError as e:
1333 self.log.debug('%s' % e)
1334 return False
James E. Blair247cab72017-07-20 16:52:36 -07001335 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001336 commit_message = '%s-1' % change.subject
1337 self.log.debug("Checking if build %s has changes; commit_message "
1338 "%s; repo_messages %s" % (self, commit_message,
1339 repo_messages))
1340 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001341 self.log.debug(" messages do not match")
1342 return False
1343 self.log.debug(" OK")
1344 return True
1345
James E. Blaird8af5422017-05-24 13:59:40 -07001346 def getWorkspaceRepos(self, projects):
1347 """Return workspace git repo objects for the listed projects
1348
1349 :arg list projects: A list of strings, each the canonical name
1350 of a project.
1351
1352 :returns: A dictionary of {name: repo} for every listed
1353 project.
1354 :rtype: dict
1355
1356 """
1357
1358 repos = {}
1359 for project in projects:
1360 path = os.path.join(self.jobdir.src_root, project)
1361 repo = git.Repo(path)
1362 repos[project] = repo
1363 return repos
1364
Clark Boylanb640e052014-04-03 16:41:46 -07001365
Paul Belanger174a8272017-03-14 13:20:10 -04001366class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1367 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001368
Paul Belanger174a8272017-03-14 13:20:10 -04001369 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001370 they will report that they have started but then pause until
1371 released before reporting completion. This attribute may be
1372 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001373 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001374 be explicitly released.
1375
1376 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001377 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001378 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001379 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001380 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001381 self.hold_jobs_in_build = False
1382 self.lock = threading.Lock()
1383 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001384 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001385 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001386 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001387
James E. Blaira5dba232016-08-08 15:53:24 -07001388 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001389 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001390
1391 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001392 :arg Change change: The :py:class:`~tests.base.FakeChange`
1393 instance which should cause the job to fail. This job
1394 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001395
1396 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 l = self.fail_tests.get(name, [])
1398 l.append(change)
1399 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001400
James E. Blair962220f2016-08-03 11:22:38 -07001401 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001402 """Release a held build.
1403
1404 :arg str regex: A regular expression which, if supplied, will
1405 cause only builds with matching names to be released. If
1406 not supplied, all builds will be released.
1407
1408 """
James E. Blair962220f2016-08-03 11:22:38 -07001409 builds = self.running_builds[:]
1410 self.log.debug("Releasing build %s (%s)" % (regex,
1411 len(self.running_builds)))
1412 for build in builds:
1413 if not regex or re.match(regex, build.name):
1414 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001415 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001416 build.release()
1417 else:
1418 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001419 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001420 self.log.debug("Done releasing builds %s (%s)" %
1421 (regex, len(self.running_builds)))
1422
Paul Belanger174a8272017-03-14 13:20:10 -04001423 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001424 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001425 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001426 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001427 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001428 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001429 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001430 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001431 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1432 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001433
1434 def stopJob(self, job):
1435 self.log.debug("handle stop")
1436 parameters = json.loads(job.arguments)
1437 uuid = parameters['uuid']
1438 for build in self.running_builds:
1439 if build.unique == uuid:
1440 build.aborted = True
1441 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001442 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001443
James E. Blaira002b032017-04-18 10:35:48 -07001444 def stop(self):
1445 for build in self.running_builds:
1446 build.release()
1447 super(RecordingExecutorServer, self).stop()
1448
Joshua Hesketh50c21782016-10-13 21:34:14 +11001449
Paul Belanger174a8272017-03-14 13:20:10 -04001450class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001451 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001452 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001453 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001454 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001455 if not commit: # merge conflict
1456 self.recordResult('MERGER_FAILURE')
1457 return commit
1458
1459 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001460 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001461 self.executor_server.lock.acquire()
1462 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001463 BuildHistory(name=build.name, result=result, changes=build.changes,
1464 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001465 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001466 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001467 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001468 )
Paul Belanger174a8272017-03-14 13:20:10 -04001469 self.executor_server.running_builds.remove(build)
1470 del self.executor_server.job_builds[self.job.unique]
1471 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001472
1473 def runPlaybooks(self, args):
1474 build = self.executor_server.job_builds[self.job.unique]
1475 build.jobdir = self.jobdir
1476
1477 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1478 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001479 return result
1480
James E. Blair892cca62017-08-09 11:36:58 -07001481 def runAnsible(self, cmd, timeout, playbook):
Paul Belanger174a8272017-03-14 13:20:10 -04001482 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001483
Paul Belanger174a8272017-03-14 13:20:10 -04001484 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001485 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair892cca62017-08-09 11:36:58 -07001486 cmd, timeout, playbook)
James E. Blair412fba82017-01-26 15:00:50 -08001487 else:
James E. Blairf641f4f2017-09-27 10:10:54 -07001488 if playbook.path:
1489 result = build.run()
1490 else:
1491 result = (self.RESULT_NORMAL, 0)
James E. Blaire1767bc2016-08-02 10:00:27 -07001492 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001493
James E. Blairad8dca02017-02-21 11:48:32 -05001494 def getHostList(self, args):
1495 self.log.debug("hostlist")
1496 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001497 for host in hosts:
1498 host['host_vars']['ansible_connection'] = 'local'
1499
1500 hosts.append(dict(
1501 name='localhost',
1502 host_vars=dict(ansible_connection='local'),
1503 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001504 return hosts
1505
James E. Blairf5dbd002015-12-23 15:26:17 -08001506
Clark Boylanb640e052014-04-03 16:41:46 -07001507class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001508 """A Gearman server for use in tests.
1509
1510 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1511 added to the queue but will not be distributed to workers
1512 until released. This attribute may be changed at any time and
1513 will take effect for subsequently enqueued jobs, but
1514 previously held jobs will still need to be explicitly
1515 released.
1516
1517 """
1518
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001519 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001520 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001521 if use_ssl:
1522 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1523 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1524 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1525 else:
1526 ssl_ca = None
1527 ssl_cert = None
1528 ssl_key = None
1529
1530 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1531 ssl_cert=ssl_cert,
1532 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001533
1534 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001535 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1536 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001537 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001538 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001539 job.waiting = self.hold_jobs_in_queue
1540 else:
1541 job.waiting = False
1542 if job.waiting:
1543 continue
1544 if job.name in connection.functions:
1545 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001546 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001547 connection.related_jobs[job.handle] = job
1548 job.worker_connection = connection
1549 job.running = True
1550 return job
1551 return None
1552
1553 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001554 """Release a held job.
1555
1556 :arg str regex: A regular expression which, if supplied, will
1557 cause only jobs with matching names to be released. If
1558 not supplied, all jobs will be released.
1559 """
Clark Boylanb640e052014-04-03 16:41:46 -07001560 released = False
1561 qlen = (len(self.high_queue) + len(self.normal_queue) +
1562 len(self.low_queue))
1563 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1564 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001565 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001566 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001567 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001568 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001569 self.log.debug("releasing queued job %s" %
1570 job.unique)
1571 job.waiting = False
1572 released = True
1573 else:
1574 self.log.debug("not releasing queued job %s" %
1575 job.unique)
1576 if released:
1577 self.wakeConnections()
1578 qlen = (len(self.high_queue) + len(self.normal_queue) +
1579 len(self.low_queue))
1580 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1581
1582
1583class FakeSMTP(object):
1584 log = logging.getLogger('zuul.FakeSMTP')
1585
1586 def __init__(self, messages, server, port):
1587 self.server = server
1588 self.port = port
1589 self.messages = messages
1590
1591 def sendmail(self, from_email, to_email, msg):
1592 self.log.info("Sending email from %s, to %s, with msg %s" % (
1593 from_email, to_email, msg))
1594
1595 headers = msg.split('\n\n', 1)[0]
1596 body = msg.split('\n\n', 1)[1]
1597
1598 self.messages.append(dict(
1599 from_email=from_email,
1600 to_email=to_email,
1601 msg=msg,
1602 headers=headers,
1603 body=body,
1604 ))
1605
1606 return True
1607
1608 def quit(self):
1609 return True
1610
1611
James E. Blairdce6cea2016-12-20 16:45:32 -08001612class FakeNodepool(object):
1613 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001614 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001615
1616 log = logging.getLogger("zuul.test.FakeNodepool")
1617
1618 def __init__(self, host, port, chroot):
1619 self.client = kazoo.client.KazooClient(
1620 hosts='%s:%s%s' % (host, port, chroot))
1621 self.client.start()
1622 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001623 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001624 self.thread = threading.Thread(target=self.run)
1625 self.thread.daemon = True
1626 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001627 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001628
1629 def stop(self):
1630 self._running = False
1631 self.thread.join()
1632 self.client.stop()
1633 self.client.close()
1634
1635 def run(self):
1636 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001637 try:
1638 self._run()
1639 except Exception:
1640 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001641 time.sleep(0.1)
1642
1643 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001644 if self.paused:
1645 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001646 for req in self.getNodeRequests():
1647 self.fulfillRequest(req)
1648
1649 def getNodeRequests(self):
1650 try:
1651 reqids = self.client.get_children(self.REQUEST_ROOT)
1652 except kazoo.exceptions.NoNodeError:
1653 return []
1654 reqs = []
1655 for oid in sorted(reqids):
1656 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001657 try:
1658 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001659 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001660 data['_oid'] = oid
1661 reqs.append(data)
1662 except kazoo.exceptions.NoNodeError:
1663 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001664 return reqs
1665
James E. Blaire18d4602017-01-05 11:17:28 -08001666 def getNodes(self):
1667 try:
1668 nodeids = self.client.get_children(self.NODE_ROOT)
1669 except kazoo.exceptions.NoNodeError:
1670 return []
1671 nodes = []
1672 for oid in sorted(nodeids):
1673 path = self.NODE_ROOT + '/' + oid
1674 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001675 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001676 data['_oid'] = oid
1677 try:
1678 lockfiles = self.client.get_children(path + '/lock')
1679 except kazoo.exceptions.NoNodeError:
1680 lockfiles = []
1681 if lockfiles:
1682 data['_lock'] = True
1683 else:
1684 data['_lock'] = False
1685 nodes.append(data)
1686 return nodes
1687
James E. Blaira38c28e2017-01-04 10:33:20 -08001688 def makeNode(self, request_id, node_type):
1689 now = time.time()
1690 path = '/nodepool/nodes/'
1691 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001692 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001693 provider='test-provider',
1694 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001695 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001696 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001697 public_ipv4='127.0.0.1',
1698 private_ipv4=None,
1699 public_ipv6=None,
1700 allocated_to=request_id,
1701 state='ready',
1702 state_time=now,
1703 created_time=now,
1704 updated_time=now,
1705 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001706 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001707 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001708 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001709 path = self.client.create(path, data,
1710 makepath=True,
1711 sequence=True)
1712 nodeid = path.split("/")[-1]
1713 return nodeid
1714
James E. Blair6ab79e02017-01-06 10:10:17 -08001715 def addFailRequest(self, request):
1716 self.fail_requests.add(request['_oid'])
1717
James E. Blairdce6cea2016-12-20 16:45:32 -08001718 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001719 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001720 return
1721 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001722 oid = request['_oid']
1723 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001724
James E. Blair6ab79e02017-01-06 10:10:17 -08001725 if oid in self.fail_requests:
1726 request['state'] = 'failed'
1727 else:
1728 request['state'] = 'fulfilled'
1729 nodes = []
1730 for node in request['node_types']:
1731 nodeid = self.makeNode(oid, node)
1732 nodes.append(nodeid)
1733 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001734
James E. Blaira38c28e2017-01-04 10:33:20 -08001735 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001736 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001737 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001738 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001739 try:
1740 self.client.set(path, data)
1741 except kazoo.exceptions.NoNodeError:
1742 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001743
1744
James E. Blair498059b2016-12-20 13:50:13 -08001745class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001746 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001747 super(ChrootedKazooFixture, self).__init__()
1748
1749 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1750 if ':' in zk_host:
1751 host, port = zk_host.split(':')
1752 else:
1753 host = zk_host
1754 port = None
1755
1756 self.zookeeper_host = host
1757
1758 if not port:
1759 self.zookeeper_port = 2181
1760 else:
1761 self.zookeeper_port = int(port)
1762
Clark Boylan621ec9a2017-04-07 17:41:33 -07001763 self.test_id = test_id
1764
James E. Blair498059b2016-12-20 13:50:13 -08001765 def _setUp(self):
1766 # Make sure the test chroot paths do not conflict
1767 random_bits = ''.join(random.choice(string.ascii_lowercase +
1768 string.ascii_uppercase)
1769 for x in range(8))
1770
Clark Boylan621ec9a2017-04-07 17:41:33 -07001771 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001772 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1773
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001774 self.addCleanup(self._cleanup)
1775
James E. Blair498059b2016-12-20 13:50:13 -08001776 # Ensure the chroot path exists and clean up any pre-existing znodes.
1777 _tmp_client = kazoo.client.KazooClient(
1778 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1779 _tmp_client.start()
1780
1781 if _tmp_client.exists(self.zookeeper_chroot):
1782 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1783
1784 _tmp_client.ensure_path(self.zookeeper_chroot)
1785 _tmp_client.stop()
1786 _tmp_client.close()
1787
James E. Blair498059b2016-12-20 13:50:13 -08001788 def _cleanup(self):
1789 '''Remove the chroot path.'''
1790 # Need a non-chroot'ed client to remove the chroot path
1791 _tmp_client = kazoo.client.KazooClient(
1792 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1793 _tmp_client.start()
1794 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1795 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001796 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001797
1798
Joshua Heskethd78b4482015-09-14 16:56:34 -06001799class MySQLSchemaFixture(fixtures.Fixture):
1800 def setUp(self):
1801 super(MySQLSchemaFixture, self).setUp()
1802
1803 random_bits = ''.join(random.choice(string.ascii_lowercase +
1804 string.ascii_uppercase)
1805 for x in range(8))
1806 self.name = '%s_%s' % (random_bits, os.getpid())
1807 self.passwd = uuid.uuid4().hex
1808 db = pymysql.connect(host="localhost",
1809 user="openstack_citest",
1810 passwd="openstack_citest",
1811 db="openstack_citest")
1812 cur = db.cursor()
1813 cur.execute("create database %s" % self.name)
1814 cur.execute(
1815 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1816 (self.name, self.name, self.passwd))
1817 cur.execute("flush privileges")
1818
1819 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1820 self.passwd,
1821 self.name)
1822 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1823 self.addCleanup(self.cleanup)
1824
1825 def cleanup(self):
1826 db = pymysql.connect(host="localhost",
1827 user="openstack_citest",
1828 passwd="openstack_citest",
1829 db="openstack_citest")
1830 cur = db.cursor()
1831 cur.execute("drop database %s" % self.name)
1832 cur.execute("drop user '%s'@'localhost'" % self.name)
1833 cur.execute("flush privileges")
1834
1835
Maru Newby3fe5f852015-01-13 04:22:14 +00001836class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001837 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001838 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001839
James E. Blair1c236df2017-02-01 14:07:24 -08001840 def attachLogs(self, *args):
1841 def reader():
1842 self._log_stream.seek(0)
1843 while True:
1844 x = self._log_stream.read(4096)
1845 if not x:
1846 break
1847 yield x.encode('utf8')
1848 content = testtools.content.content_from_reader(
1849 reader,
1850 testtools.content_type.UTF8_TEXT,
1851 False)
1852 self.addDetail('logging', content)
1853
Clark Boylanb640e052014-04-03 16:41:46 -07001854 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001855 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001856 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1857 try:
1858 test_timeout = int(test_timeout)
1859 except ValueError:
1860 # If timeout value is invalid do not set a timeout.
1861 test_timeout = 0
1862 if test_timeout > 0:
1863 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1864
1865 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1866 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1867 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1868 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1869 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1870 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1871 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1872 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1873 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1874 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001875 self._log_stream = StringIO()
1876 self.addOnException(self.attachLogs)
1877 else:
1878 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001879
James E. Blair73b41772017-05-22 13:22:55 -07001880 # NOTE(jeblair): this is temporary extra debugging to try to
1881 # track down a possible leak.
1882 orig_git_repo_init = git.Repo.__init__
1883
1884 def git_repo_init(myself, *args, **kw):
1885 orig_git_repo_init(myself, *args, **kw)
1886 self.log.debug("Created git repo 0x%x %s" %
1887 (id(myself), repr(myself)))
1888
1889 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1890 git_repo_init))
1891
James E. Blair1c236df2017-02-01 14:07:24 -08001892 handler = logging.StreamHandler(self._log_stream)
1893 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1894 '%(levelname)-8s %(message)s')
1895 handler.setFormatter(formatter)
1896
1897 logger = logging.getLogger()
1898 logger.setLevel(logging.DEBUG)
1899 logger.addHandler(handler)
1900
Clark Boylan3410d532017-04-25 12:35:29 -07001901 # Make sure we don't carry old handlers around in process state
1902 # which slows down test runs
1903 self.addCleanup(logger.removeHandler, handler)
1904 self.addCleanup(handler.close)
1905 self.addCleanup(handler.flush)
1906
James E. Blair1c236df2017-02-01 14:07:24 -08001907 # NOTE(notmorgan): Extract logging overrides for specific
1908 # libraries from the OS_LOG_DEFAULTS env and create loggers
1909 # for each. This is used to limit the output during test runs
1910 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001911 log_defaults_from_env = os.environ.get(
1912 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001913 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001914
James E. Blairdce6cea2016-12-20 16:45:32 -08001915 if log_defaults_from_env:
1916 for default in log_defaults_from_env.split(','):
1917 try:
1918 name, level_str = default.split('=', 1)
1919 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001920 logger = logging.getLogger(name)
1921 logger.setLevel(level)
1922 logger.addHandler(handler)
1923 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001924 except ValueError:
1925 # NOTE(notmorgan): Invalid format of the log default,
1926 # skip and don't try and apply a logger for the
1927 # specified module
1928 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001929
Maru Newby3fe5f852015-01-13 04:22:14 +00001930
1931class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001932 """A test case with a functioning Zuul.
1933
1934 The following class variables are used during test setup and can
1935 be overidden by subclasses but are effectively read-only once a
1936 test method starts running:
1937
1938 :cvar str config_file: This points to the main zuul config file
1939 within the fixtures directory. Subclasses may override this
1940 to obtain a different behavior.
1941
1942 :cvar str tenant_config_file: This is the tenant config file
1943 (which specifies from what git repos the configuration should
1944 be loaded). It defaults to the value specified in
1945 `config_file` but can be overidden by subclasses to obtain a
1946 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001947 configuration. See also the :py:func:`simple_layout`
1948 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001949
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001950 :cvar bool create_project_keys: Indicates whether Zuul should
1951 auto-generate keys for each project, or whether the test
1952 infrastructure should insert dummy keys to save time during
1953 startup. Defaults to False.
1954
James E. Blaire7b99a02016-08-05 14:27:34 -07001955 The following are instance variables that are useful within test
1956 methods:
1957
1958 :ivar FakeGerritConnection fake_<connection>:
1959 A :py:class:`~tests.base.FakeGerritConnection` will be
1960 instantiated for each connection present in the config file
1961 and stored here. For instance, `fake_gerrit` will hold the
1962 FakeGerritConnection object for a connection named `gerrit`.
1963
1964 :ivar FakeGearmanServer gearman_server: An instance of
1965 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1966 server that all of the Zuul components in this test use to
1967 communicate with each other.
1968
Paul Belanger174a8272017-03-14 13:20:10 -04001969 :ivar RecordingExecutorServer executor_server: An instance of
1970 :py:class:`~tests.base.RecordingExecutorServer` which is the
1971 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001972
1973 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1974 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001975 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001976 list upon completion.
1977
1978 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1979 objects representing completed builds. They are appended to
1980 the list in the order they complete.
1981
1982 """
1983
James E. Blair83005782015-12-11 14:46:03 -08001984 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001985 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001986 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001987 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001988
1989 def _startMerger(self):
1990 self.merge_server = zuul.merger.server.MergeServer(self.config,
1991 self.connections)
1992 self.merge_server.start()
1993
Maru Newby3fe5f852015-01-13 04:22:14 +00001994 def setUp(self):
1995 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001996
1997 self.setupZK()
1998
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001999 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07002000 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002001 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2002 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002003 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002004 tmp_root = tempfile.mkdtemp(
2005 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002006 self.test_root = os.path.join(tmp_root, "zuul-test")
2007 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002008 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002009 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002010 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002011 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2012 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002013
2014 if os.path.exists(self.test_root):
2015 shutil.rmtree(self.test_root)
2016 os.makedirs(self.test_root)
2017 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002018 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002019 os.makedirs(self.merger_state_root)
2020 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002021
2022 # Make per test copy of Configuration.
2023 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002024 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2025 if not os.path.exists(self.private_key_file):
2026 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2027 shutil.copy(src_private_key_file, self.private_key_file)
2028 shutil.copy('{}.pub'.format(src_private_key_file),
2029 '{}.pub'.format(self.private_key_file))
2030 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002031 self.config.set('scheduler', 'tenant_config',
2032 os.path.join(
2033 FIXTURE_DIR,
2034 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002035 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002036 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002037 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002038 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002039 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002040
Clark Boylanb640e052014-04-03 16:41:46 -07002041 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10002042 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
2043 # see: https://github.com/jsocol/pystatsd/issues/61
2044 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07002045 os.environ['STATSD_PORT'] = str(self.statsd.port)
2046 self.statsd.start()
2047 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05002048 importlib.reload(statsd)
2049 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07002050
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002051 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002052
2053 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002054 self.log.info("Gearman server on port %s" %
2055 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002056 if self.use_ssl:
2057 self.log.info('SSL enabled for gearman')
2058 self.config.set(
2059 'gearman', 'ssl_ca',
2060 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2061 self.config.set(
2062 'gearman', 'ssl_cert',
2063 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2064 self.config.set(
2065 'gearman', 'ssl_key',
2066 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002067
James E. Blaire511d2f2016-12-08 15:22:26 -08002068 gerritsource.GerritSource.replication_timeout = 1.5
2069 gerritsource.GerritSource.replication_retry_interval = 0.5
2070 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002071
Joshua Hesketh352264b2015-08-11 23:42:08 +10002072 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07002073
Jan Hruban7083edd2015-08-21 14:00:54 +02002074 self.webapp = zuul.webapp.WebApp(
2075 self.sched, port=0, listen_address='127.0.0.1')
2076
Jan Hruban6b71aff2015-10-22 16:58:08 +02002077 self.event_queues = [
2078 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002079 self.sched.trigger_event_queue,
2080 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002081 ]
2082
James E. Blairfef78942016-03-11 16:28:56 -08002083 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002084 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002085
Paul Belanger174a8272017-03-14 13:20:10 -04002086 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002087 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002088 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002089 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002090 _test_root=self.test_root,
2091 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002092 self.executor_server.start()
2093 self.history = self.executor_server.build_history
2094 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002095
Paul Belanger174a8272017-03-14 13:20:10 -04002096 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002097 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002098 self.merge_client = zuul.merger.client.MergeClient(
2099 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002100 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002101 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002102 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002103
James E. Blair0d5a36e2017-02-21 10:53:44 -05002104 self.fake_nodepool = FakeNodepool(
2105 self.zk_chroot_fixture.zookeeper_host,
2106 self.zk_chroot_fixture.zookeeper_port,
2107 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002108
Paul Belanger174a8272017-03-14 13:20:10 -04002109 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002110 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002111 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002112 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002113
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002114 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002115
2116 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002117 self.webapp.start()
2118 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002119 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002120 # Cleanups are run in reverse order
2121 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002122 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002123 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002124
James E. Blairb9c0d772017-03-03 14:34:49 -08002125 self.sched.reconfigure(self.config)
2126 self.sched.resume()
2127
Tobias Henkel7df274b2017-05-26 17:41:11 +02002128 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002129 # Set up gerrit related fakes
2130 # Set a changes database so multiple FakeGerrit's can report back to
2131 # a virtual canonical database given by the configured hostname
2132 self.gerrit_changes_dbs = {}
2133
2134 def getGerritConnection(driver, name, config):
2135 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2136 con = FakeGerritConnection(driver, name, config,
2137 changes_db=db,
2138 upstream_root=self.upstream_root)
2139 self.event_queues.append(con.event_queue)
2140 setattr(self, 'fake_' + name, con)
2141 return con
2142
2143 self.useFixture(fixtures.MonkeyPatch(
2144 'zuul.driver.gerrit.GerritDriver.getConnection',
2145 getGerritConnection))
2146
Gregory Haynes4fc12542015-04-22 20:38:06 -07002147 def getGithubConnection(driver, name, config):
2148 con = FakeGithubConnection(driver, name, config,
2149 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002150 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002151 setattr(self, 'fake_' + name, con)
2152 return con
2153
2154 self.useFixture(fixtures.MonkeyPatch(
2155 'zuul.driver.github.GithubDriver.getConnection',
2156 getGithubConnection))
2157
James E. Blaire511d2f2016-12-08 15:22:26 -08002158 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002159 # TODO(jhesketh): This should come from lib.connections for better
2160 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002161 # Register connections from the config
2162 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002163
Joshua Hesketh352264b2015-08-11 23:42:08 +10002164 def FakeSMTPFactory(*args, **kw):
2165 args = [self.smtp_messages] + list(args)
2166 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002167
Joshua Hesketh352264b2015-08-11 23:42:08 +10002168 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002169
James E. Blaire511d2f2016-12-08 15:22:26 -08002170 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002171 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002172 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002173
James E. Blair83005782015-12-11 14:46:03 -08002174 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002175 # This creates the per-test configuration object. It can be
2176 # overriden by subclasses, but should not need to be since it
2177 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002178 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002179 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002180
James E. Blair39840362017-06-23 20:34:02 +01002181 sections = ['zuul', 'scheduler', 'executor', 'merger']
2182 for section in sections:
2183 if not self.config.has_section(section):
2184 self.config.add_section(section)
2185
James E. Blair06cc3922017-04-19 10:08:10 -07002186 if not self.setupSimpleLayout():
2187 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002188 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002189 self.tenant_config_file)
2190 git_path = os.path.join(
2191 os.path.dirname(
2192 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2193 'git')
2194 if os.path.exists(git_path):
2195 for reponame in os.listdir(git_path):
2196 project = reponame.replace('_', '/')
2197 self.copyDirToRepo(project,
2198 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002199 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002200 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002201 self.setupAllProjectKeys()
2202
James E. Blair06cc3922017-04-19 10:08:10 -07002203 def setupSimpleLayout(self):
2204 # If the test method has been decorated with a simple_layout,
2205 # use that instead of the class tenant_config_file. Set up a
2206 # single config-project with the specified layout, and
2207 # initialize repos for all of the 'project' entries which
2208 # appear in the layout.
2209 test_name = self.id().split('.')[-1]
2210 test = getattr(self, test_name)
2211 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002212 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002213 else:
2214 return False
2215
James E. Blairb70e55a2017-04-19 12:57:02 -07002216 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002217 path = os.path.join(FIXTURE_DIR, path)
2218 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002219 data = f.read()
2220 layout = yaml.safe_load(data)
2221 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002222 untrusted_projects = []
2223 for item in layout:
2224 if 'project' in item:
2225 name = item['project']['name']
2226 untrusted_projects.append(name)
2227 self.init_repo(name)
2228 self.addCommitToRepo(name, 'initial commit',
2229 files={'README': ''},
2230 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002231 if 'job' in item:
2232 jobname = item['job']['name']
2233 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002234
2235 root = os.path.join(self.test_root, "config")
2236 if not os.path.exists(root):
2237 os.makedirs(root)
2238 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2239 config = [{'tenant':
2240 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002241 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002242 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002243 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002244 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002245 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002246 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002247 os.path.join(FIXTURE_DIR, f.name))
2248
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002249 self.init_repo('org/common-config')
2250 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002251 files, branch='master', tag='init')
2252
2253 return True
2254
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002255 def setupAllProjectKeys(self):
2256 if self.create_project_keys:
2257 return
2258
James E. Blair39840362017-06-23 20:34:02 +01002259 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002260 with open(os.path.join(FIXTURE_DIR, path)) as f:
2261 tenant_config = yaml.safe_load(f.read())
2262 for tenant in tenant_config:
2263 sources = tenant['tenant']['source']
2264 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002265 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002266 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002267 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002268 self.setupProjectKeys(source, project)
2269
2270 def setupProjectKeys(self, source, project):
2271 # Make sure we set up an RSA key for the project so that we
2272 # don't spend time generating one:
2273
James E. Blair6459db12017-06-29 14:57:20 -07002274 if isinstance(project, dict):
2275 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002276 key_root = os.path.join(self.state_root, 'keys')
2277 if not os.path.isdir(key_root):
2278 os.mkdir(key_root, 0o700)
2279 private_key_file = os.path.join(key_root, source, project + '.pem')
2280 private_key_dir = os.path.dirname(private_key_file)
2281 self.log.debug("Installing test keys for project %s at %s" % (
2282 project, private_key_file))
2283 if not os.path.isdir(private_key_dir):
2284 os.makedirs(private_key_dir)
2285 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2286 with open(private_key_file, 'w') as o:
2287 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002288
James E. Blair498059b2016-12-20 13:50:13 -08002289 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002290 self.zk_chroot_fixture = self.useFixture(
2291 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002292 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002293 self.zk_chroot_fixture.zookeeper_host,
2294 self.zk_chroot_fixture.zookeeper_port,
2295 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002296
James E. Blair96c6bf82016-01-15 16:20:40 -08002297 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002298 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002299
2300 files = {}
2301 for (dirpath, dirnames, filenames) in os.walk(source_path):
2302 for filename in filenames:
2303 test_tree_filepath = os.path.join(dirpath, filename)
2304 common_path = os.path.commonprefix([test_tree_filepath,
2305 source_path])
2306 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2307 with open(test_tree_filepath, 'r') as f:
2308 content = f.read()
2309 files[relative_filepath] = content
2310 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002311 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002312
James E. Blaire18d4602017-01-05 11:17:28 -08002313 def assertNodepoolState(self):
2314 # Make sure that there are no pending requests
2315
2316 requests = self.fake_nodepool.getNodeRequests()
2317 self.assertEqual(len(requests), 0)
2318
2319 nodes = self.fake_nodepool.getNodes()
2320 for node in nodes:
2321 self.assertFalse(node['_lock'], "Node %s is locked" %
2322 (node['_oid'],))
2323
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002324 def assertNoGeneratedKeys(self):
2325 # Make sure that Zuul did not generate any project keys
2326 # (unless it was supposed to).
2327
2328 if self.create_project_keys:
2329 return
2330
2331 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2332 test_key = i.read()
2333
2334 key_root = os.path.join(self.state_root, 'keys')
2335 for root, dirname, files in os.walk(key_root):
2336 for fn in files:
2337 with open(os.path.join(root, fn)) as f:
2338 self.assertEqual(test_key, f.read())
2339
Clark Boylanb640e052014-04-03 16:41:46 -07002340 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002341 self.log.debug("Assert final state")
2342 # Make sure no jobs are running
2343 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002344 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002345 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002346 gc.collect()
2347 for obj in gc.get_objects():
2348 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002349 self.log.debug("Leaked git repo object: 0x%x %s" %
2350 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002351 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002352 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002353 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002354 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002355 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002356 for tenant in self.sched.abide.tenants.values():
2357 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002358 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002359 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002360
2361 def shutdown(self):
2362 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002363 self.executor_server.hold_jobs_in_build = False
2364 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002365 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002366 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002367 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002368 self.sched.stop()
2369 self.sched.join()
2370 self.statsd.stop()
2371 self.statsd.join()
2372 self.webapp.stop()
2373 self.webapp.join()
2374 self.rpc.stop()
2375 self.rpc.join()
2376 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002377 self.fake_nodepool.stop()
2378 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002379 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002380 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002381 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002382 # Further the pydevd threads also need to be whitelisted so debugging
2383 # e.g. in PyCharm is possible without breaking shutdown.
2384 whitelist = ['executor-watchdog',
2385 'pydevd.CommandThread',
2386 'pydevd.Reader',
2387 'pydevd.Writer',
2388 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002389 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002390 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002391 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002392 log_str = ""
2393 for thread_id, stack_frame in sys._current_frames().items():
2394 log_str += "Thread: %s\n" % thread_id
2395 log_str += "".join(traceback.format_stack(stack_frame))
2396 self.log.debug(log_str)
2397 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002398
James E. Blaira002b032017-04-18 10:35:48 -07002399 def assertCleanShutdown(self):
2400 pass
2401
James E. Blairc4ba97a2017-04-19 16:26:24 -07002402 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002403 parts = project.split('/')
2404 path = os.path.join(self.upstream_root, *parts[:-1])
2405 if not os.path.exists(path):
2406 os.makedirs(path)
2407 path = os.path.join(self.upstream_root, project)
2408 repo = git.Repo.init(path)
2409
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002410 with repo.config_writer() as config_writer:
2411 config_writer.set_value('user', 'email', 'user@example.com')
2412 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002413
Clark Boylanb640e052014-04-03 16:41:46 -07002414 repo.index.commit('initial commit')
2415 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002416 if tag:
2417 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002418
James E. Blair97d902e2014-08-21 13:25:56 -07002419 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002420 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002421 repo.git.clean('-x', '-f', '-d')
2422
James E. Blair97d902e2014-08-21 13:25:56 -07002423 def create_branch(self, project, branch):
2424 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002425 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002426 fn = os.path.join(path, 'README')
2427
2428 branch_head = repo.create_head(branch)
2429 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002430 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002431 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002432 f.close()
2433 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002434 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002435
James E. Blair97d902e2014-08-21 13:25:56 -07002436 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002437 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002438 repo.git.clean('-x', '-f', '-d')
2439
Sachi King9f16d522016-03-16 12:20:45 +11002440 def create_commit(self, project):
2441 path = os.path.join(self.upstream_root, project)
2442 repo = git.Repo(path)
2443 repo.head.reference = repo.heads['master']
2444 file_name = os.path.join(path, 'README')
2445 with open(file_name, 'a') as f:
2446 f.write('creating fake commit\n')
2447 repo.index.add([file_name])
2448 commit = repo.index.commit('Creating a fake commit')
2449 return commit.hexsha
2450
James E. Blairf4a5f022017-04-18 14:01:10 -07002451 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002452 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002453 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002454 while len(self.builds):
2455 self.release(self.builds[0])
2456 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002457 i += 1
2458 if count is not None and i >= count:
2459 break
James E. Blairb8c16472015-05-05 14:55:26 -07002460
James E. Blairdf25ddc2017-07-08 07:57:09 -07002461 def getSortedBuilds(self):
2462 "Return the list of currently running builds sorted by name"
2463
2464 return sorted(self.builds, key=lambda x: x.name)
2465
Clark Boylanb640e052014-04-03 16:41:46 -07002466 def release(self, job):
2467 if isinstance(job, FakeBuild):
2468 job.release()
2469 else:
2470 job.waiting = False
2471 self.log.debug("Queued job %s released" % job.unique)
2472 self.gearman_server.wakeConnections()
2473
2474 def getParameter(self, job, name):
2475 if isinstance(job, FakeBuild):
2476 return job.parameters[name]
2477 else:
2478 parameters = json.loads(job.arguments)
2479 return parameters[name]
2480
Clark Boylanb640e052014-04-03 16:41:46 -07002481 def haveAllBuildsReported(self):
2482 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002483 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002484 return False
2485 # Find out if every build that the worker has completed has been
2486 # reported back to Zuul. If it hasn't then that means a Gearman
2487 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002488 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002489 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002490 if not zbuild:
2491 # It has already been reported
2492 continue
2493 # It hasn't been reported yet.
2494 return False
2495 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002496 worker = self.executor_server.executor_worker
2497 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002498 if connection.state == 'GRAB_WAIT':
2499 return False
2500 return True
2501
2502 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002503 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002504 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002505 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002506 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002507 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002508 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002509 for j in conn.related_jobs.values():
2510 if j.unique == build.uuid:
2511 client_job = j
2512 break
2513 if not client_job:
2514 self.log.debug("%s is not known to the gearman client" %
2515 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002516 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002517 if not client_job.handle:
2518 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002519 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002520 server_job = self.gearman_server.jobs.get(client_job.handle)
2521 if not server_job:
2522 self.log.debug("%s is not known to the gearman server" %
2523 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002524 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002525 if not hasattr(server_job, 'waiting'):
2526 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002527 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002528 if server_job.waiting:
2529 continue
James E. Blair17302972016-08-10 16:11:42 -07002530 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002531 self.log.debug("%s has not reported start" % build)
2532 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002533 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002534 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002535 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002536 if worker_build:
2537 if worker_build.isWaiting():
2538 continue
2539 else:
2540 self.log.debug("%s is running" % worker_build)
2541 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002542 else:
James E. Blair962220f2016-08-03 11:22:38 -07002543 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002544 return False
James E. Blaira002b032017-04-18 10:35:48 -07002545 for (build_uuid, job_worker) in \
2546 self.executor_server.job_workers.items():
2547 if build_uuid not in seen_builds:
2548 self.log.debug("%s is not finalized" % build_uuid)
2549 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002550 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002551
James E. Blairdce6cea2016-12-20 16:45:32 -08002552 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002553 if self.fake_nodepool.paused:
2554 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002555 if self.sched.nodepool.requests:
2556 return False
2557 return True
2558
Jan Hruban6b71aff2015-10-22 16:58:08 +02002559 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002560 for event_queue in self.event_queues:
2561 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002562
2563 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002564 for event_queue in self.event_queues:
2565 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002566
Clark Boylanb640e052014-04-03 16:41:46 -07002567 def waitUntilSettled(self):
2568 self.log.debug("Waiting until settled...")
2569 start = time.time()
2570 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002571 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002572 self.log.error("Timeout waiting for Zuul to settle")
2573 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002574 for event_queue in self.event_queues:
2575 self.log.error(" %s: %s" %
2576 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002577 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002578 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002579 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002580 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002581 self.log.error("All requests completed: %s" %
2582 (self.areAllNodeRequestsComplete(),))
2583 self.log.error("Merge client jobs: %s" %
2584 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002585 raise Exception("Timeout waiting for Zuul to settle")
2586 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002587
Paul Belanger174a8272017-03-14 13:20:10 -04002588 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002589 # have all build states propogated to zuul?
2590 if self.haveAllBuildsReported():
2591 # Join ensures that the queue is empty _and_ events have been
2592 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002593 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002594 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002595 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002596 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002597 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002598 self.areAllNodeRequestsComplete() and
2599 all(self.eventQueuesEmpty())):
2600 # The queue empty check is placed at the end to
2601 # ensure that if a component adds an event between
2602 # when locked the run handler and checked that the
2603 # components were stable, we don't erroneously
2604 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002605 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002606 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002607 self.log.debug("...settled.")
2608 return
2609 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002610 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002611 self.sched.wake_event.wait(0.1)
2612
2613 def countJobResults(self, jobs, result):
2614 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002615 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002616
Monty Taylor0d926122017-05-24 08:07:56 -05002617 def getBuildByName(self, name):
2618 for build in self.builds:
2619 if build.name == name:
2620 return build
2621 raise Exception("Unable to find build %s" % name)
2622
James E. Blair96c6bf82016-01-15 16:20:40 -08002623 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002624 for job in self.history:
2625 if (job.name == name and
2626 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002627 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002628 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002629 raise Exception("Unable to find job %s in history" % name)
2630
2631 def assertEmptyQueues(self):
2632 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002633 for tenant in self.sched.abide.tenants.values():
2634 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002635 for pipeline_queue in pipeline.queues:
2636 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002637 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002638 pipeline.name, pipeline_queue.name,
2639 pipeline_queue.queue))
2640 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002641 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002642
2643 def assertReportedStat(self, key, value=None, kind=None):
2644 start = time.time()
2645 while time.time() < (start + 5):
2646 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002647 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002648 if key == k:
2649 if value is None and kind is None:
2650 return
2651 elif value:
2652 if value == v:
2653 return
2654 elif kind:
2655 if v.endswith('|' + kind):
2656 return
2657 time.sleep(0.1)
2658
Clark Boylanb640e052014-04-03 16:41:46 -07002659 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002660
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002661 def assertBuilds(self, builds):
2662 """Assert that the running builds are as described.
2663
2664 The list of running builds is examined and must match exactly
2665 the list of builds described by the input.
2666
2667 :arg list builds: A list of dictionaries. Each item in the
2668 list must match the corresponding build in the build
2669 history, and each element of the dictionary must match the
2670 corresponding attribute of the build.
2671
2672 """
James E. Blair3158e282016-08-19 09:34:11 -07002673 try:
2674 self.assertEqual(len(self.builds), len(builds))
2675 for i, d in enumerate(builds):
2676 for k, v in d.items():
2677 self.assertEqual(
2678 getattr(self.builds[i], k), v,
2679 "Element %i in builds does not match" % (i,))
2680 except Exception:
2681 for build in self.builds:
2682 self.log.error("Running build: %s" % build)
2683 else:
2684 self.log.error("No running builds")
2685 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002686
James E. Blairb536ecc2016-08-31 10:11:42 -07002687 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002688 """Assert that the completed builds are as described.
2689
2690 The list of completed builds is examined and must match
2691 exactly the list of builds described by the input.
2692
2693 :arg list history: A list of dictionaries. Each item in the
2694 list must match the corresponding build in the build
2695 history, and each element of the dictionary must match the
2696 corresponding attribute of the build.
2697
James E. Blairb536ecc2016-08-31 10:11:42 -07002698 :arg bool ordered: If true, the history must match the order
2699 supplied, if false, the builds are permitted to have
2700 arrived in any order.
2701
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002702 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002703 def matches(history_item, item):
2704 for k, v in item.items():
2705 if getattr(history_item, k) != v:
2706 return False
2707 return True
James E. Blair3158e282016-08-19 09:34:11 -07002708 try:
2709 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002710 if ordered:
2711 for i, d in enumerate(history):
2712 if not matches(self.history[i], d):
2713 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002714 "Element %i in history does not match %s" %
2715 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002716 else:
2717 unseen = self.history[:]
2718 for i, d in enumerate(history):
2719 found = False
2720 for unseen_item in unseen:
2721 if matches(unseen_item, d):
2722 found = True
2723 unseen.remove(unseen_item)
2724 break
2725 if not found:
2726 raise Exception("No match found for element %i "
2727 "in history" % (i,))
2728 if unseen:
2729 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002730 except Exception:
2731 for build in self.history:
2732 self.log.error("Completed build: %s" % build)
2733 else:
2734 self.log.error("No completed builds")
2735 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002736
James E. Blair6ac368c2016-12-22 18:07:20 -08002737 def printHistory(self):
2738 """Log the build history.
2739
2740 This can be useful during tests to summarize what jobs have
2741 completed.
2742
2743 """
2744 self.log.debug("Build history:")
2745 for build in self.history:
2746 self.log.debug(build)
2747
James E. Blair59fdbac2015-12-07 17:08:06 -08002748 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002749 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2750
James E. Blair9ea70072017-04-19 16:05:30 -07002751 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002752 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002753 if not os.path.exists(root):
2754 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002755 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2756 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002757- tenant:
2758 name: openstack
2759 source:
2760 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002761 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002762 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002763 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002764 - org/project
2765 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002766 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002767 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002768 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002769 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002770 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002771
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002772 def addCommitToRepo(self, project, message, files,
2773 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002774 path = os.path.join(self.upstream_root, project)
2775 repo = git.Repo(path)
2776 repo.head.reference = branch
2777 zuul.merger.merger.reset_repo_to_head(repo)
2778 for fn, content in files.items():
2779 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002780 try:
2781 os.makedirs(os.path.dirname(fn))
2782 except OSError:
2783 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002784 with open(fn, 'w') as f:
2785 f.write(content)
2786 repo.index.add([fn])
2787 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002788 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002789 repo.heads[branch].commit = commit
2790 repo.head.reference = branch
2791 repo.git.clean('-x', '-f', '-d')
2792 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002793 if tag:
2794 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002795 return before
2796
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002797 def commitConfigUpdate(self, project_name, source_name):
2798 """Commit an update to zuul.yaml
2799
2800 This overwrites the zuul.yaml in the specificed project with
2801 the contents specified.
2802
2803 :arg str project_name: The name of the project containing
2804 zuul.yaml (e.g., common-config)
2805
2806 :arg str source_name: The path to the file (underneath the
2807 test fixture directory) whose contents should be used to
2808 replace zuul.yaml.
2809 """
2810
2811 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002812 files = {}
2813 with open(source_path, 'r') as f:
2814 data = f.read()
2815 layout = yaml.safe_load(data)
2816 files['zuul.yaml'] = data
2817 for item in layout:
2818 if 'job' in item:
2819 jobname = item['job']['name']
2820 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002821 before = self.addCommitToRepo(
2822 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002823 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002824 return before
2825
Clint Byrum627ba362017-08-14 13:20:40 -07002826 def newTenantConfig(self, source_name):
2827 """ Use this to update the tenant config file in tests
2828
2829 This will update self.tenant_config_file to point to a temporary file
2830 for the duration of this particular test. The content of that file will
2831 be taken from FIXTURE_DIR/source_name
2832
2833 After the test the original value of self.tenant_config_file will be
2834 restored.
2835
2836 :arg str source_name: The path of the file under
2837 FIXTURE_DIR that will be used to populate the new tenant
2838 config file.
2839 """
2840 source_path = os.path.join(FIXTURE_DIR, source_name)
2841 orig_tenant_config_file = self.tenant_config_file
2842 with tempfile.NamedTemporaryFile(
2843 delete=False, mode='wb') as new_tenant_config:
2844 self.tenant_config_file = new_tenant_config.name
2845 with open(source_path, mode='rb') as source_tenant_config:
2846 new_tenant_config.write(source_tenant_config.read())
2847 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2848 self.setupAllProjectKeys()
2849 self.log.debug(
2850 'tenant_config_file = {}'.format(self.tenant_config_file))
2851
2852 def _restoreTenantConfig():
2853 self.log.debug(
2854 'restoring tenant_config_file = {}'.format(
2855 orig_tenant_config_file))
2856 os.unlink(self.tenant_config_file)
2857 self.tenant_config_file = orig_tenant_config_file
2858 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2859 self.addCleanup(_restoreTenantConfig)
2860
James E. Blair7fc8daa2016-08-08 15:37:15 -07002861 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002862
James E. Blair7fc8daa2016-08-08 15:37:15 -07002863 """Inject a Fake (Gerrit) event.
2864
2865 This method accepts a JSON-encoded event and simulates Zuul
2866 having received it from Gerrit. It could (and should)
2867 eventually apply to any connection type, but is currently only
2868 used with Gerrit connections. The name of the connection is
2869 used to look up the corresponding server, and the event is
2870 simulated as having been received by all Zuul connections
2871 attached to that server. So if two Gerrit connections in Zuul
2872 are connected to the same Gerrit server, and you invoke this
2873 method specifying the name of one of them, the event will be
2874 received by both.
2875
2876 .. note::
2877
2878 "self.fake_gerrit.addEvent" calls should be migrated to
2879 this method.
2880
2881 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002882 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002883 :arg str event: The JSON-encoded event.
2884
2885 """
2886 specified_conn = self.connections.connections[connection]
2887 for conn in self.connections.connections.values():
2888 if (isinstance(conn, specified_conn.__class__) and
2889 specified_conn.server == conn.server):
2890 conn.addEvent(event)
2891
James E. Blaird8af5422017-05-24 13:59:40 -07002892 def getUpstreamRepos(self, projects):
2893 """Return upstream git repo objects for the listed projects
2894
2895 :arg list projects: A list of strings, each the canonical name
2896 of a project.
2897
2898 :returns: A dictionary of {name: repo} for every listed
2899 project.
2900 :rtype: dict
2901
2902 """
2903
2904 repos = {}
2905 for project in projects:
2906 # FIXME(jeblair): the upstream root does not yet have a
2907 # hostname component; that needs to be added, and this
2908 # line removed:
2909 tmp_project_name = '/'.join(project.split('/')[1:])
2910 path = os.path.join(self.upstream_root, tmp_project_name)
2911 repo = git.Repo(path)
2912 repos[project] = repo
2913 return repos
2914
James E. Blair3f876d52016-07-22 13:07:14 -07002915
2916class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002917 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002918 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002919
Jamie Lennox7655b552017-03-17 12:33:38 +11002920 @contextmanager
2921 def jobLog(self, build):
2922 """Print job logs on assertion errors
2923
2924 This method is a context manager which, if it encounters an
2925 ecxeption, adds the build log to the debug output.
2926
2927 :arg Build build: The build that's being asserted.
2928 """
2929 try:
2930 yield
2931 except Exception:
2932 path = os.path.join(self.test_root, build.uuid,
2933 'work', 'logs', 'job-output.txt')
2934 with open(path) as f:
2935 self.log.debug(f.read())
2936 raise
2937
Joshua Heskethd78b4482015-09-14 16:56:34 -06002938
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002939class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002940 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002941 use_ssl = True
2942
2943
Joshua Heskethd78b4482015-09-14 16:56:34 -06002944class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002945 def setup_config(self):
2946 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002947 for section_name in self.config.sections():
2948 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2949 section_name, re.I)
2950 if not con_match:
2951 continue
2952
2953 if self.config.get(section_name, 'driver') == 'sql':
2954 f = MySQLSchemaFixture()
2955 self.useFixture(f)
2956 if (self.config.get(section_name, 'dburi') ==
2957 '$MYSQL_FIXTURE_DBURI$'):
2958 self.config.set(section_name, 'dburi', f.dburi)