blob: 59c0d2ade9174f75806c33526637c69719de546f [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 -050023from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043import git
44import gear
45import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080046import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080047import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060048import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070049import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blair6bacffb2018-01-05 13:45:25 -080055import tests.fakegithub
James E. Blaire511d2f2016-12-08 15:22:26 -080056import zuul.driver.gerrit.gerritsource as gerritsource
57import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070058import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.scheduler
60import zuul.webapp
Paul Belanger174a8272017-03-14 13:20:10 -040061import zuul.executor.server
62import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080063import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070065import zuul.merger.merger
66import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020067import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070068import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080069import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070070import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010071from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070072
73FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
74 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080075
76KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070077
Clark Boylanb640e052014-04-03 16:41:46 -070078
79def repack_repo(path):
80 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
81 output = subprocess.Popen(cmd, close_fds=True,
82 stdout=subprocess.PIPE,
83 stderr=subprocess.PIPE)
84 out = output.communicate()
85 if output.returncode:
86 raise Exception("git repack returned %d" % output.returncode)
87 return out
88
89
90def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040091 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070092
93
James E. Blaira190f3b2015-01-05 14:56:54 -080094def iterate_timeout(max_seconds, purpose):
95 start = time.time()
96 count = 0
97 while (time.time() < start + max_seconds):
98 count += 1
99 yield count
100 time.sleep(0)
101 raise Exception("Timeout waiting for %s" % purpose)
102
103
Jesse Keating436a5452017-04-20 11:48:41 -0700104def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700105 """Specify a layout file for use by a test method.
106
107 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700108 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700109
110 Some tests require only a very simple configuration. For those,
111 establishing a complete config directory hierachy is too much
112 work. In those cases, you can add a simple zuul.yaml file to the
113 test fixtures directory (in fixtures/layouts/foo.yaml) and use
114 this decorator to indicate the test method should use that rather
115 than the tenant config file specified by the test class.
116
117 The decorator will cause that layout file to be added to a
118 config-project called "common-config" and each "project" instance
119 referenced in the layout file will have a git repo automatically
120 initialized.
121 """
122
123 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700124 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700125 return test
126 return decorator
127
128
Gregory Haynes4fc12542015-04-22 20:38:06 -0700129class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700130 _common_path_default = "refs/changes"
131 _points_to_commits_only = True
132
133
Gregory Haynes4fc12542015-04-22 20:38:06 -0700134class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200135 categories = {'Approved': ('Approved', -1, 1),
136 'Code-Review': ('Code-Review', -2, 2),
137 'Verified': ('Verified', -2, 2)}
138
Clark Boylanb640e052014-04-03 16:41:46 -0700139 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700140 status='NEW', upstream_root=None, files={},
141 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700176 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair289f5932017-07-27 15:02:29 -0700180 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700183 if parent is None:
184 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700185 ref = GerritChangeReference.create(
186 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700187 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700189 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.git.clean('-x', '-f', '-d')
191
192 path = os.path.join(self.upstream_root, self.project)
193 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for fn, content in files.items():
195 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700196 if content is None:
197 os.unlink(fn)
198 repo.index.remove([fn])
199 else:
200 d = os.path.dirname(fn)
201 if not os.path.exists(d):
202 os.makedirs(d)
203 with open(fn, 'w') as f:
204 f.write(content)
205 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700206 else:
207 for fni in range(100):
208 fn = os.path.join(path, str(fni))
209 f = open(fn, 'w')
210 for ci in range(4096):
211 f.write(random.choice(string.printable))
212 f.close()
213 repo.index.add([fn])
214
215 r = repo.index.commit(msg)
216 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700217 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700218 repo.git.clean('-x', '-f', '-d')
219 repo.heads['master'].checkout()
220 return r
221
James E. Blair289f5932017-07-27 15:02:29 -0700222 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700223 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700225 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 data = ("test %s %s %s\n" %
227 (self.branch, self.number, self.latest_patchset))
228 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700229 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700230 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700231 ps_files = [{'file': '/COMMIT_MSG',
232 'type': 'ADDED'},
233 {'file': 'README',
234 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700235 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700236 ps_files.append({'file': f, 'type': 'ADDED'})
237 d = {'approvals': [],
238 'createdOn': time.time(),
239 'files': ps_files,
240 'number': str(self.latest_patchset),
241 'ref': 'refs/changes/1/%s/%s' % (self.number,
242 self.latest_patchset),
243 'revision': c.hexsha,
244 'uploader': {'email': 'user@example.com',
245 'name': 'User name',
246 'username': 'user'}}
247 self.data['currentPatchSet'] = d
248 self.patchsets.append(d)
249 self.data['submitRecords'] = self.getSubmitRecords()
250
251 def getPatchsetCreatedEvent(self, patchset):
252 event = {"type": "patchset-created",
253 "change": {"project": self.project,
254 "branch": self.branch,
255 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
256 "number": str(self.number),
257 "subject": self.subject,
258 "owner": {"name": "User Name"},
259 "url": "https://hostname/3"},
260 "patchSet": self.patchsets[patchset - 1],
261 "uploader": {"name": "User Name"}}
262 return event
263
264 def getChangeRestoredEvent(self):
265 event = {"type": "change-restored",
266 "change": {"project": self.project,
267 "branch": self.branch,
268 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
269 "number": str(self.number),
270 "subject": self.subject,
271 "owner": {"name": "User Name"},
272 "url": "https://hostname/3"},
273 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100274 "patchSet": self.patchsets[-1],
275 "reason": ""}
276 return event
277
278 def getChangeAbandonedEvent(self):
279 event = {"type": "change-abandoned",
280 "change": {"project": self.project,
281 "branch": self.branch,
282 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
283 "number": str(self.number),
284 "subject": self.subject,
285 "owner": {"name": "User Name"},
286 "url": "https://hostname/3"},
287 "abandoner": {"name": "User Name"},
288 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700289 "reason": ""}
290 return event
291
292 def getChangeCommentEvent(self, patchset):
293 event = {"type": "comment-added",
294 "change": {"project": self.project,
295 "branch": self.branch,
296 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
297 "number": str(self.number),
298 "subject": self.subject,
299 "owner": {"name": "User Name"},
300 "url": "https://hostname/3"},
301 "patchSet": self.patchsets[patchset - 1],
302 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200303 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700304 "description": "Code-Review",
305 "value": "0"}],
306 "comment": "This is a comment"}
307 return event
308
James E. Blairc2a5ed72017-02-20 14:12:01 -0500309 def getChangeMergedEvent(self):
310 event = {"submitter": {"name": "Jenkins",
311 "username": "jenkins"},
312 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
313 "patchSet": self.patchsets[-1],
314 "change": self.data,
315 "type": "change-merged",
316 "eventCreatedOn": 1487613810}
317 return event
318
James E. Blair8cce42e2016-10-18 08:18:36 -0700319 def getRefUpdatedEvent(self):
320 path = os.path.join(self.upstream_root, self.project)
321 repo = git.Repo(path)
322 oldrev = repo.heads[self.branch].commit.hexsha
323
324 event = {
325 "type": "ref-updated",
326 "submitter": {
327 "name": "User Name",
328 },
329 "refUpdate": {
330 "oldRev": oldrev,
331 "newRev": self.patchsets[-1]['revision'],
332 "refName": self.branch,
333 "project": self.project,
334 }
335 }
336 return event
337
Joshua Hesketh642824b2014-07-01 17:54:59 +1000338 def addApproval(self, category, value, username='reviewer_john',
339 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700340 if not granted_on:
341 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000342 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200343 'description': self.categories[category][0],
344 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000345 'value': str(value),
346 'by': {
347 'username': username,
348 'email': username + '@example.com',
349 },
350 'grantedOn': int(granted_on)
351 }
Clark Boylanb640e052014-04-03 16:41:46 -0700352 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200353 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700354 del self.patchsets[-1]['approvals'][i]
355 self.patchsets[-1]['approvals'].append(approval)
356 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000357 'author': {'email': 'author@example.com',
358 'name': 'Patchset Author',
359 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'change': {'branch': self.branch,
361 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
362 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000363 'owner': {'email': 'owner@example.com',
364 'name': 'Change Owner',
365 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700366 'project': self.project,
367 'subject': self.subject,
368 'topic': 'master',
369 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000370 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700371 'patchSet': self.patchsets[-1],
372 'type': 'comment-added'}
373 self.data['submitRecords'] = self.getSubmitRecords()
374 return json.loads(json.dumps(event))
375
376 def getSubmitRecords(self):
377 status = {}
378 for cat in self.categories.keys():
379 status[cat] = 0
380
381 for a in self.patchsets[-1]['approvals']:
382 cur = status[a['type']]
383 cat_min, cat_max = self.categories[a['type']][1:]
384 new = int(a['value'])
385 if new == cat_min:
386 cur = new
387 elif abs(new) > abs(cur):
388 cur = new
389 status[a['type']] = cur
390
391 labels = []
392 ok = True
393 for typ, cat in self.categories.items():
394 cur = status[typ]
395 cat_min, cat_max = cat[1:]
396 if cur == cat_min:
397 value = 'REJECT'
398 ok = False
399 elif cur == cat_max:
400 value = 'OK'
401 else:
402 value = 'NEED'
403 ok = False
404 labels.append({'label': cat[0], 'status': value})
405 if ok:
406 return [{'status': 'OK'}]
407 return [{'status': 'NOT_READY',
408 'labels': labels}]
409
410 def setDependsOn(self, other, patchset):
411 self.depends_on_change = other
412 d = {'id': other.data['id'],
413 'number': other.data['number'],
414 'ref': other.patchsets[patchset - 1]['ref']
415 }
416 self.data['dependsOn'] = [d]
417
418 other.needed_by_changes.append(self)
419 needed = other.data.get('neededBy', [])
420 d = {'id': self.data['id'],
421 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700422 'ref': self.patchsets[-1]['ref'],
423 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700424 }
425 needed.append(d)
426 other.data['neededBy'] = needed
427
428 def query(self):
429 self.queried += 1
430 d = self.data.get('dependsOn')
431 if d:
432 d = d[0]
433 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
434 d['isCurrentPatchSet'] = True
435 else:
436 d['isCurrentPatchSet'] = False
437 return json.loads(json.dumps(self.data))
438
439 def setMerged(self):
440 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000441 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700442 return
443 if self.fail_merge:
444 return
445 self.data['status'] = 'MERGED'
446 self.open = False
447
448 path = os.path.join(self.upstream_root, self.project)
449 repo = git.Repo(path)
450 repo.heads[self.branch].commit = \
451 repo.commit(self.patchsets[-1]['revision'])
452
453 def setReported(self):
454 self.reported += 1
455
456
James E. Blaire511d2f2016-12-08 15:22:26 -0800457class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700458 """A Fake Gerrit connection for use in tests.
459
460 This subclasses
461 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
462 ability for tests to add changes to the fake Gerrit it represents.
463 """
464
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700466
James E. Blaire511d2f2016-12-08 15:22:26 -0800467 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700468 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800469 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 connection_config)
471
Monty Taylorb934c1a2017-06-16 19:31:47 -0500472 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
474 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000475 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700476 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200477 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700478
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700479 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700480 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700481 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700482 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700483 c = FakeGerritChange(self, self.change_number, project, branch,
484 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700485 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700486 self.changes[self.change_number] = c
487 return c
488
James E. Blair1edfd972017-12-01 15:54:24 -0800489 def addFakeTag(self, project, branch, tag):
490 path = os.path.join(self.upstream_root, project)
491 repo = git.Repo(path)
492 commit = repo.heads[branch].commit
493 newrev = commit.hexsha
494 ref = 'refs/tags/' + tag
495
496 git.Tag.create(repo, tag, commit)
497
498 event = {
499 "type": "ref-updated",
500 "submitter": {
501 "name": "User Name",
502 },
503 "refUpdate": {
504 "oldRev": 40 * '0',
505 "newRev": newrev,
506 "refName": ref,
507 "project": project,
508 }
509 }
510 return event
511
James E. Blair72facdc2017-08-17 10:29:12 -0700512 def getFakeBranchCreatedEvent(self, project, branch):
513 path = os.path.join(self.upstream_root, project)
514 repo = git.Repo(path)
515 oldrev = 40 * '0'
516
517 event = {
518 "type": "ref-updated",
519 "submitter": {
520 "name": "User Name",
521 },
522 "refUpdate": {
523 "oldRev": oldrev,
524 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700525 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700526 "project": project,
527 }
528 }
529 return event
530
Clark Boylanb640e052014-04-03 16:41:46 -0700531 def review(self, project, changeid, message, action):
532 number, ps = changeid.split(',')
533 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000534
535 # Add the approval back onto the change (ie simulate what gerrit would
536 # do).
537 # Usually when zuul leaves a review it'll create a feedback loop where
538 # zuul's review enters another gerrit event (which is then picked up by
539 # zuul). However, we can't mimic this behaviour (by adding this
540 # approval event into the queue) as it stops jobs from checking what
541 # happens before this event is triggered. If a job needs to see what
542 # happens they can add their own verified event into the queue.
543 # Nevertheless, we can update change with the new review in gerrit.
544
James E. Blair8b5408c2016-08-08 15:37:46 -0700545 for cat in action.keys():
546 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000547 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000548
Clark Boylanb640e052014-04-03 16:41:46 -0700549 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000550
Clark Boylanb640e052014-04-03 16:41:46 -0700551 if 'submit' in action:
552 change.setMerged()
553 if message:
554 change.setReported()
555
556 def query(self, number):
557 change = self.changes.get(int(number))
558 if change:
559 return change.query()
560 return {}
561
James E. Blairc494d542014-08-06 09:23:52 -0700562 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700563 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700564 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800565 if query.startswith('change:'):
566 # Query a specific changeid
567 changeid = query[len('change:'):]
568 l = [change.query() for change in self.changes.values()
569 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700570 elif query.startswith('message:'):
571 # Query the content of a commit message
572 msg = query[len('message:'):].strip()
573 l = [change.query() for change in self.changes.values()
574 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800575 else:
576 # Query all open changes
577 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700578 return l
James E. Blairc494d542014-08-06 09:23:52 -0700579
Joshua Hesketh352264b2015-08-11 23:42:08 +1000580 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700581 pass
582
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200583 def _uploadPack(self, project):
584 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
585 'multi_ack thin-pack side-band side-band-64k ofs-delta '
586 'shallow no-progress include-tag multi_ack_detailed no-done\n')
587 path = os.path.join(self.upstream_root, project.name)
588 repo = git.Repo(path)
589 for ref in repo.refs:
590 r = ref.object.hexsha + ' ' + ref.path + '\n'
591 ret += '%04x%s' % (len(r) + 4, r)
592 ret += '0000'
593 return ret
594
Joshua Hesketh352264b2015-08-11 23:42:08 +1000595 def getGitUrl(self, project):
596 return os.path.join(self.upstream_root, project.name)
597
Clark Boylanb640e052014-04-03 16:41:46 -0700598
Gregory Haynes4fc12542015-04-22 20:38:06 -0700599class GithubChangeReference(git.Reference):
600 _common_path_default = "refs/pull"
601 _points_to_commits_only = True
602
603
604class FakeGithubPullRequest(object):
605
606 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800607 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700608 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700609 """Creates a new PR with several commits.
610 Sends an event about opened PR."""
611 self.github = github
612 self.source = github
613 self.number = number
614 self.project = project
615 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100616 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700617 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100618 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700619 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100620 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700621 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100622 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100623 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800624 self.reviews = []
625 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700626 self.updated_at = None
627 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100628 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100629 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700630 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700631 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100632 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700633 self._updateTimeStamp()
634
Jan Hruban570d01c2016-03-10 21:51:32 +0100635 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700636 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100637 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700638 self._updateTimeStamp()
639
Jan Hruban570d01c2016-03-10 21:51:32 +0100640 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700641 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100642 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700643 self._updateTimeStamp()
644
645 def getPullRequestOpenedEvent(self):
646 return self._getPullRequestEvent('opened')
647
648 def getPullRequestSynchronizeEvent(self):
649 return self._getPullRequestEvent('synchronize')
650
651 def getPullRequestReopenedEvent(self):
652 return self._getPullRequestEvent('reopened')
653
654 def getPullRequestClosedEvent(self):
655 return self._getPullRequestEvent('closed')
656
Jesse Keatinga41566f2017-06-14 18:17:51 -0700657 def getPullRequestEditedEvent(self):
658 return self._getPullRequestEvent('edited')
659
Gregory Haynes4fc12542015-04-22 20:38:06 -0700660 def addComment(self, message):
661 self.comments.append(message)
662 self._updateTimeStamp()
663
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200664 def getCommentAddedEvent(self, text):
665 name = 'issue_comment'
666 data = {
667 'action': 'created',
668 'issue': {
669 'number': self.number
670 },
671 'comment': {
672 'body': text
673 },
674 'repository': {
675 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100676 },
677 'sender': {
678 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200679 }
680 }
681 return (name, data)
682
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800683 def getReviewAddedEvent(self, review):
684 name = 'pull_request_review'
685 data = {
686 'action': 'submitted',
687 'pull_request': {
688 'number': self.number,
689 'title': self.subject,
690 'updated_at': self.updated_at,
691 'base': {
692 'ref': self.branch,
693 'repo': {
694 'full_name': self.project
695 }
696 },
697 'head': {
698 'sha': self.head_sha
699 }
700 },
701 'review': {
702 'state': review
703 },
704 'repository': {
705 'full_name': self.project
706 },
707 'sender': {
708 'login': 'ghuser'
709 }
710 }
711 return (name, data)
712
Jan Hruban16ad31f2015-11-07 14:39:07 +0100713 def addLabel(self, name):
714 if name not in self.labels:
715 self.labels.append(name)
716 self._updateTimeStamp()
717 return self._getLabelEvent(name)
718
719 def removeLabel(self, name):
720 if name in self.labels:
721 self.labels.remove(name)
722 self._updateTimeStamp()
723 return self._getUnlabelEvent(name)
724
725 def _getLabelEvent(self, label):
726 name = 'pull_request'
727 data = {
728 'action': 'labeled',
729 'pull_request': {
730 'number': self.number,
731 'updated_at': self.updated_at,
732 'base': {
733 'ref': self.branch,
734 'repo': {
735 'full_name': self.project
736 }
737 },
738 'head': {
739 'sha': self.head_sha
740 }
741 },
742 'label': {
743 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100744 },
745 'sender': {
746 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100747 }
748 }
749 return (name, data)
750
751 def _getUnlabelEvent(self, label):
752 name = 'pull_request'
753 data = {
754 'action': 'unlabeled',
755 'pull_request': {
756 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100757 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100758 'updated_at': self.updated_at,
759 'base': {
760 'ref': self.branch,
761 'repo': {
762 'full_name': self.project
763 }
764 },
765 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800766 'sha': self.head_sha,
767 'repo': {
768 'full_name': self.project
769 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100770 }
771 },
772 'label': {
773 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100774 },
775 'sender': {
776 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100777 }
778 }
779 return (name, data)
780
Jesse Keatinga41566f2017-06-14 18:17:51 -0700781 def editBody(self, body):
782 self.body = body
783 self._updateTimeStamp()
784
Gregory Haynes4fc12542015-04-22 20:38:06 -0700785 def _getRepo(self):
786 repo_path = os.path.join(self.upstream_root, self.project)
787 return git.Repo(repo_path)
788
789 def _createPRRef(self):
790 repo = self._getRepo()
791 GithubChangeReference.create(
792 repo, self._getPRReference(), 'refs/tags/init')
793
Jan Hruban570d01c2016-03-10 21:51:32 +0100794 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700795 repo = self._getRepo()
796 ref = repo.references[self._getPRReference()]
797 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100798 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700799 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100800 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700801 repo.head.reference = ref
802 zuul.merger.merger.reset_repo_to_head(repo)
803 repo.git.clean('-x', '-f', '-d')
804
Jan Hruban570d01c2016-03-10 21:51:32 +0100805 if files:
806 fn = files[0]
807 self.files = files
808 else:
809 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
810 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100811 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700812 fn = os.path.join(repo.working_dir, fn)
813 f = open(fn, 'w')
814 with open(fn, 'w') as f:
815 f.write("test %s %s\n" %
816 (self.branch, self.number))
817 repo.index.add([fn])
818
819 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800820 # Create an empty set of statuses for the given sha,
821 # each sha on a PR may have a status set on it
822 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700823 repo.head.reference = 'master'
824 zuul.merger.merger.reset_repo_to_head(repo)
825 repo.git.clean('-x', '-f', '-d')
826 repo.heads['master'].checkout()
827
828 def _updateTimeStamp(self):
829 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
830
831 def getPRHeadSha(self):
832 repo = self._getRepo()
833 return repo.references[self._getPRReference()].commit.hexsha
834
Jesse Keatingae4cd272017-01-30 17:10:44 -0800835 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800836 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
837 # convert the timestamp to a str format that would be returned
838 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800839
Adam Gandelmand81dd762017-02-09 15:15:49 -0800840 if granted_on:
841 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
842 submitted_at = time.strftime(
843 gh_time_format, granted_on.timetuple())
844 else:
845 # github timestamps only down to the second, so we need to make
846 # sure reviews that tests add appear to be added over a period of
847 # time in the past and not all at once.
848 if not self.reviews:
849 # the first review happens 10 mins ago
850 offset = 600
851 else:
852 # subsequent reviews happen 1 minute closer to now
853 offset = 600 - (len(self.reviews) * 60)
854
855 granted_on = datetime.datetime.utcfromtimestamp(
856 time.time() - offset)
857 submitted_at = time.strftime(
858 gh_time_format, granted_on.timetuple())
859
Jesse Keatingae4cd272017-01-30 17:10:44 -0800860 self.reviews.append({
861 'state': state,
862 'user': {
863 'login': user,
864 'email': user + "@derp.com",
865 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800866 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800867 })
868
Gregory Haynes4fc12542015-04-22 20:38:06 -0700869 def _getPRReference(self):
870 return '%s/head' % self.number
871
872 def _getPullRequestEvent(self, action):
873 name = 'pull_request'
874 data = {
875 'action': action,
876 'number': self.number,
877 'pull_request': {
878 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100879 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700880 'updated_at': self.updated_at,
881 'base': {
882 'ref': self.branch,
883 'repo': {
884 'full_name': self.project
885 }
886 },
887 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800888 'sha': self.head_sha,
889 'repo': {
890 'full_name': self.project
891 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700892 },
893 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100894 },
895 'sender': {
896 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700897 }
898 }
899 return (name, data)
900
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800901 def getCommitStatusEvent(self, context, state='success', user='zuul'):
902 name = 'status'
903 data = {
904 'state': state,
905 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700906 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800907 'description': 'Test results for %s: %s' % (self.head_sha, state),
908 'target_url': 'http://zuul/%s' % self.head_sha,
909 'branches': [],
910 'context': context,
911 'sender': {
912 'login': user
913 }
914 }
915 return (name, data)
916
James E. Blair289f5932017-07-27 15:02:29 -0700917 def setMerged(self, commit_message):
918 self.is_merged = True
919 self.merge_message = commit_message
920
921 repo = self._getRepo()
922 repo.heads[self.branch].commit = repo.commit(self.head_sha)
923
Gregory Haynes4fc12542015-04-22 20:38:06 -0700924
925class FakeGithubConnection(githubconnection.GithubConnection):
926 log = logging.getLogger("zuul.test.FakeGithubConnection")
927
928 def __init__(self, driver, connection_name, connection_config,
James E. Blair6bacffb2018-01-05 13:45:25 -0800929 changes_db=None, upstream_root=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700930 super(FakeGithubConnection, self).__init__(driver, connection_name,
931 connection_config)
932 self.connection_name = connection_name
933 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800934 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700935 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700936 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100937 self.merge_failure = False
938 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100939 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800940 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Tobias Henkel64e37a02017-08-02 10:13:30 +0200941
942 def getGithubClient(self,
943 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600944 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200945 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700946
Jesse Keatinga41566f2017-06-14 18:17:51 -0700947 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700948 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700949 self.pr_number += 1
950 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100951 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700952 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800953 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700954 return pull_request
955
Jesse Keating71a47ff2017-06-06 11:36:43 -0700956 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
957 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700958 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700959 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700960 if not new_rev:
961 new_rev = random_sha1()
962 name = 'push'
963 data = {
964 'ref': ref,
965 'before': old_rev,
966 'after': new_rev,
967 'repository': {
968 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700969 },
970 'commits': [
971 {
972 'added': added_files,
973 'removed': removed_files,
974 'modified': modified_files
975 }
976 ]
Wayne1a78c612015-06-11 17:14:13 -0700977 }
978 return (name, data)
979
Gregory Haynes4fc12542015-04-22 20:38:06 -0700980 def emitEvent(self, event):
981 """Emulates sending the GitHub webhook event to the connection."""
982 port = self.webapp.server.socket.getsockname()[1]
983 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700984 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -0700985 secret = self.connection_config['webhook_token']
986 signature = githubconnection._sign_request(payload, secret)
987 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700988 req = urllib.request.Request(
989 'http://localhost:%s/connection/%s/payload'
990 % (port, self.connection_name),
991 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000992 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700993
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200994 def addProject(self, project):
995 # use the original method here and additionally register it in the
996 # fake github
997 super(FakeGithubConnection, self).addProject(project)
998 self.getGithubClient(project).addProject(project)
999
Jesse Keating9021a012017-08-29 14:45:27 -07001000 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001001 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001002 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001003 if len(prs) > 1:
1004 raise Exception('Multiple pulls found with head sha: %s' % sha)
1005 pr = prs[0]
1006 return self.getPull(pr.project, pr.number)
1007
Jesse Keatingae4cd272017-01-30 17:10:44 -08001008 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001009 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001010 return pr.reviews
1011
Jesse Keatingae4cd272017-01-30 17:10:44 -08001012 def getRepoPermission(self, project, login):
1013 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001014 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001015 pr_owner, pr_project = pr.project.split('/')
1016 if (pr_owner == owner and proj == pr_project):
1017 if login in pr.writers:
1018 return 'write'
1019 else:
1020 return 'read'
1021
Gregory Haynes4fc12542015-04-22 20:38:06 -07001022 def getGitUrl(self, project):
1023 return os.path.join(self.upstream_root, str(project))
1024
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001025 def real_getGitUrl(self, project):
1026 return super(FakeGithubConnection, self).getGitUrl(project)
1027
Jan Hrubane252a732017-01-03 15:03:09 +01001028 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001029 # record that this got reported
1030 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001031 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001032 pull_request.addComment(message)
1033
Jan Hruban3b415922016-02-03 13:10:22 +01001034 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001035 # record that this got reported
1036 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001037 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001038 if self.merge_failure:
1039 raise Exception('Pull request was not merged')
1040 if self.merge_not_allowed_count > 0:
1041 self.merge_not_allowed_count -= 1
1042 raise MergeFailure('Merge was not successful due to mergeability'
1043 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001044 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001045
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001046 def setCommitStatus(self, project, sha, state, url='', description='',
1047 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001048 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001049 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001050 super(FakeGithubConnection, self).setCommitStatus(
1051 project, sha, state,
1052 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001053
Jan Hruban16ad31f2015-11-07 14:39:07 +01001054 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001055 # record that this got reported
1056 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001057 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001058 pull_request.addLabel(label)
1059
1060 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001061 # record that this got reported
1062 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001063 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001064 pull_request.removeLabel(label)
1065
Gregory Haynes4fc12542015-04-22 20:38:06 -07001066
Clark Boylanb640e052014-04-03 16:41:46 -07001067class BuildHistory(object):
1068 def __init__(self, **kw):
1069 self.__dict__.update(kw)
1070
1071 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001072 return ("<Completed build, result: %s name: %s uuid: %s "
1073 "changes: %s ref: %s>" %
1074 (self.result, self.name, self.uuid,
1075 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001076
1077
Clark Boylanb640e052014-04-03 16:41:46 -07001078class FakeStatsd(threading.Thread):
1079 def __init__(self):
1080 threading.Thread.__init__(self)
1081 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001082 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001083 self.sock.bind(('', 0))
1084 self.port = self.sock.getsockname()[1]
1085 self.wake_read, self.wake_write = os.pipe()
1086 self.stats = []
1087
1088 def run(self):
1089 while True:
1090 poll = select.poll()
1091 poll.register(self.sock, select.POLLIN)
1092 poll.register(self.wake_read, select.POLLIN)
1093 ret = poll.poll()
1094 for (fd, event) in ret:
1095 if fd == self.sock.fileno():
1096 data = self.sock.recvfrom(1024)
1097 if not data:
1098 return
1099 self.stats.append(data[0])
1100 if fd == self.wake_read:
1101 return
1102
1103 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001104 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001105
1106
James E. Blaire1767bc2016-08-02 10:00:27 -07001107class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001108 log = logging.getLogger("zuul.test")
1109
Paul Belanger174a8272017-03-14 13:20:10 -04001110 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001111 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001112 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001113 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001114 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001115 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001116 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001117 # TODOv3(jeblair): self.node is really "the label of the node
1118 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001119 # keep using it like this, or we may end up exposing more of
1120 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001121 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001122 self.node = None
1123 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001124 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001125 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001126 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001127 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001128 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001129 self.wait_condition = threading.Condition()
1130 self.waiting = False
1131 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001132 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001133 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001134 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001135 items = self.parameters['zuul']['items']
1136 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1137 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001138
James E. Blair3158e282016-08-19 09:34:11 -07001139 def __repr__(self):
1140 waiting = ''
1141 if self.waiting:
1142 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001143 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1144 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001145
Clark Boylanb640e052014-04-03 16:41:46 -07001146 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001147 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001148 self.wait_condition.acquire()
1149 self.wait_condition.notify()
1150 self.waiting = False
1151 self.log.debug("Build %s released" % self.unique)
1152 self.wait_condition.release()
1153
1154 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001155 """Return whether this build is being held.
1156
1157 :returns: Whether the build is being held.
1158 :rtype: bool
1159 """
1160
Clark Boylanb640e052014-04-03 16:41:46 -07001161 self.wait_condition.acquire()
1162 if self.waiting:
1163 ret = True
1164 else:
1165 ret = False
1166 self.wait_condition.release()
1167 return ret
1168
1169 def _wait(self):
1170 self.wait_condition.acquire()
1171 self.waiting = True
1172 self.log.debug("Build %s waiting" % self.unique)
1173 self.wait_condition.wait()
1174 self.wait_condition.release()
1175
1176 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001177 self.log.debug('Running build %s' % self.unique)
1178
Paul Belanger174a8272017-03-14 13:20:10 -04001179 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001180 self.log.debug('Holding build %s' % self.unique)
1181 self._wait()
1182 self.log.debug("Build %s continuing" % self.unique)
1183
James E. Blair412fba82017-01-26 15:00:50 -08001184 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001185 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001186 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001187 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001188 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001189 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001190 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001191
James E. Blaire1767bc2016-08-02 10:00:27 -07001192 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001193
James E. Blaira5dba232016-08-08 15:53:24 -07001194 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001195 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001196 for change in changes:
1197 if self.hasChanges(change):
1198 return True
1199 return False
1200
James E. Blaire7b99a02016-08-05 14:27:34 -07001201 def hasChanges(self, *changes):
1202 """Return whether this build has certain changes in its git repos.
1203
1204 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001205 are expected to be present (in order) in the git repository of
1206 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001207
1208 :returns: Whether the build has the indicated changes.
1209 :rtype: bool
1210
1211 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001212 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001213 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001214 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001215 try:
1216 repo = git.Repo(path)
1217 except NoSuchPathError as e:
1218 self.log.debug('%s' % e)
1219 return False
James E. Blair247cab72017-07-20 16:52:36 -07001220 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001221 commit_message = '%s-1' % change.subject
1222 self.log.debug("Checking if build %s has changes; commit_message "
1223 "%s; repo_messages %s" % (self, commit_message,
1224 repo_messages))
1225 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001226 self.log.debug(" messages do not match")
1227 return False
1228 self.log.debug(" OK")
1229 return True
1230
James E. Blaird8af5422017-05-24 13:59:40 -07001231 def getWorkspaceRepos(self, projects):
1232 """Return workspace git repo objects for the listed projects
1233
1234 :arg list projects: A list of strings, each the canonical name
1235 of a project.
1236
1237 :returns: A dictionary of {name: repo} for every listed
1238 project.
1239 :rtype: dict
1240
1241 """
1242
1243 repos = {}
1244 for project in projects:
1245 path = os.path.join(self.jobdir.src_root, project)
1246 repo = git.Repo(path)
1247 repos[project] = repo
1248 return repos
1249
Clark Boylanb640e052014-04-03 16:41:46 -07001250
James E. Blair107bb252017-10-13 15:53:16 -07001251class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1252 def doMergeChanges(self, merger, items, repo_state):
1253 # Get a merger in order to update the repos involved in this job.
1254 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1255 merger, items, repo_state)
1256 if not commit: # merge conflict
1257 self.recordResult('MERGER_FAILURE')
1258 return commit
1259
1260 def recordResult(self, result):
1261 build = self.executor_server.job_builds[self.job.unique]
1262 self.executor_server.lock.acquire()
1263 self.executor_server.build_history.append(
1264 BuildHistory(name=build.name, result=result, changes=build.changes,
1265 node=build.node, uuid=build.unique,
1266 ref=build.parameters['zuul']['ref'],
1267 parameters=build.parameters, jobdir=build.jobdir,
1268 pipeline=build.parameters['zuul']['pipeline'])
1269 )
1270 self.executor_server.running_builds.remove(build)
1271 del self.executor_server.job_builds[self.job.unique]
1272 self.executor_server.lock.release()
1273
1274 def runPlaybooks(self, args):
1275 build = self.executor_server.job_builds[self.job.unique]
1276 build.jobdir = self.jobdir
1277
1278 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1279 self.recordResult(result)
1280 return result
1281
James E. Blaira86aaf12017-10-15 20:59:50 -07001282 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001283 build = self.executor_server.job_builds[self.job.unique]
1284
1285 if self.executor_server._run_ansible:
1286 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001287 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001288 else:
1289 if playbook.path:
1290 result = build.run()
1291 else:
1292 result = (self.RESULT_NORMAL, 0)
1293 return result
1294
1295 def getHostList(self, args):
1296 self.log.debug("hostlist")
1297 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1298 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001299 if not host['host_vars'].get('ansible_connection'):
1300 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001301
1302 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001303 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001304 host_vars=dict(ansible_connection='local'),
1305 host_keys=[]))
1306 return hosts
1307
1308
Paul Belanger174a8272017-03-14 13:20:10 -04001309class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1310 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001311
Paul Belanger174a8272017-03-14 13:20:10 -04001312 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001313 they will report that they have started but then pause until
1314 released before reporting completion. This attribute may be
1315 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001316 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001317 be explicitly released.
1318
1319 """
James E. Blairfaf81982017-10-10 15:42:26 -07001320
1321 _job_class = RecordingAnsibleJob
1322
James E. Blairf5dbd002015-12-23 15:26:17 -08001323 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001324 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001325 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001326 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001327 self.hold_jobs_in_build = False
1328 self.lock = threading.Lock()
1329 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001330 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001331 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001332 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001333
James E. Blaira5dba232016-08-08 15:53:24 -07001334 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001335 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001336
1337 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001338 :arg Change change: The :py:class:`~tests.base.FakeChange`
1339 instance which should cause the job to fail. This job
1340 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001341
1342 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001343 l = self.fail_tests.get(name, [])
1344 l.append(change)
1345 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001346
James E. Blair962220f2016-08-03 11:22:38 -07001347 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001348 """Release a held build.
1349
1350 :arg str regex: A regular expression which, if supplied, will
1351 cause only builds with matching names to be released. If
1352 not supplied, all builds will be released.
1353
1354 """
James E. Blair962220f2016-08-03 11:22:38 -07001355 builds = self.running_builds[:]
1356 self.log.debug("Releasing build %s (%s)" % (regex,
1357 len(self.running_builds)))
1358 for build in builds:
1359 if not regex or re.match(regex, build.name):
1360 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001361 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001362 build.release()
1363 else:
1364 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001365 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001366 self.log.debug("Done releasing builds %s (%s)" %
1367 (regex, len(self.running_builds)))
1368
Paul Belanger174a8272017-03-14 13:20:10 -04001369 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001370 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001371 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001372 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001373 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001374 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001375 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001376 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001377 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001378
1379 def stopJob(self, job):
1380 self.log.debug("handle stop")
1381 parameters = json.loads(job.arguments)
1382 uuid = parameters['uuid']
1383 for build in self.running_builds:
1384 if build.unique == uuid:
1385 build.aborted = True
1386 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001387 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001388
James E. Blaira002b032017-04-18 10:35:48 -07001389 def stop(self):
1390 for build in self.running_builds:
1391 build.release()
1392 super(RecordingExecutorServer, self).stop()
1393
Joshua Hesketh50c21782016-10-13 21:34:14 +11001394
Clark Boylanb640e052014-04-03 16:41:46 -07001395class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001396 """A Gearman server for use in tests.
1397
1398 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1399 added to the queue but will not be distributed to workers
1400 until released. This attribute may be changed at any time and
1401 will take effect for subsequently enqueued jobs, but
1402 previously held jobs will still need to be explicitly
1403 released.
1404
1405 """
1406
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001407 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001408 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001409 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001410 if use_ssl:
1411 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1412 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1413 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1414 else:
1415 ssl_ca = None
1416 ssl_cert = None
1417 ssl_key = None
1418
1419 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1420 ssl_cert=ssl_cert,
1421 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001422
1423 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001424 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1425 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001426 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001427 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001428 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001429 elif job.name.startswith(b'merger:'):
1430 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001431 else:
1432 job.waiting = False
1433 if job.waiting:
1434 continue
1435 if job.name in connection.functions:
1436 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001437 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001438 connection.related_jobs[job.handle] = job
1439 job.worker_connection = connection
1440 job.running = True
1441 return job
1442 return None
1443
1444 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001445 """Release a held job.
1446
1447 :arg str regex: A regular expression which, if supplied, will
1448 cause only jobs with matching names to be released. If
1449 not supplied, all jobs will be released.
1450 """
Clark Boylanb640e052014-04-03 16:41:46 -07001451 released = False
1452 qlen = (len(self.high_queue) + len(self.normal_queue) +
1453 len(self.low_queue))
1454 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1455 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001456 match = False
1457 if job.name == b'executor:execute':
1458 parameters = json.loads(job.arguments.decode('utf8'))
1459 if not regex or re.match(regex, parameters.get('job')):
1460 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001461 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001462 if not regex:
1463 match = True
1464 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001465 self.log.debug("releasing queued job %s" %
1466 job.unique)
1467 job.waiting = False
1468 released = True
1469 else:
1470 self.log.debug("not releasing queued job %s" %
1471 job.unique)
1472 if released:
1473 self.wakeConnections()
1474 qlen = (len(self.high_queue) + len(self.normal_queue) +
1475 len(self.low_queue))
1476 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1477
1478
1479class FakeSMTP(object):
1480 log = logging.getLogger('zuul.FakeSMTP')
1481
1482 def __init__(self, messages, server, port):
1483 self.server = server
1484 self.port = port
1485 self.messages = messages
1486
1487 def sendmail(self, from_email, to_email, msg):
1488 self.log.info("Sending email from %s, to %s, with msg %s" % (
1489 from_email, to_email, msg))
1490
1491 headers = msg.split('\n\n', 1)[0]
1492 body = msg.split('\n\n', 1)[1]
1493
1494 self.messages.append(dict(
1495 from_email=from_email,
1496 to_email=to_email,
1497 msg=msg,
1498 headers=headers,
1499 body=body,
1500 ))
1501
1502 return True
1503
1504 def quit(self):
1505 return True
1506
1507
James E. Blairdce6cea2016-12-20 16:45:32 -08001508class FakeNodepool(object):
1509 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001510 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001511
1512 log = logging.getLogger("zuul.test.FakeNodepool")
1513
1514 def __init__(self, host, port, chroot):
1515 self.client = kazoo.client.KazooClient(
1516 hosts='%s:%s%s' % (host, port, chroot))
1517 self.client.start()
1518 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001519 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001520 self.thread = threading.Thread(target=self.run)
1521 self.thread.daemon = True
1522 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001523 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001524
1525 def stop(self):
1526 self._running = False
1527 self.thread.join()
1528 self.client.stop()
1529 self.client.close()
1530
1531 def run(self):
1532 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001533 try:
1534 self._run()
1535 except Exception:
1536 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001537 time.sleep(0.1)
1538
1539 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001540 if self.paused:
1541 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001542 for req in self.getNodeRequests():
1543 self.fulfillRequest(req)
1544
1545 def getNodeRequests(self):
1546 try:
1547 reqids = self.client.get_children(self.REQUEST_ROOT)
1548 except kazoo.exceptions.NoNodeError:
1549 return []
1550 reqs = []
1551 for oid in sorted(reqids):
1552 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001553 try:
1554 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001555 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001556 data['_oid'] = oid
1557 reqs.append(data)
1558 except kazoo.exceptions.NoNodeError:
1559 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001560 return reqs
1561
James E. Blaire18d4602017-01-05 11:17:28 -08001562 def getNodes(self):
1563 try:
1564 nodeids = self.client.get_children(self.NODE_ROOT)
1565 except kazoo.exceptions.NoNodeError:
1566 return []
1567 nodes = []
1568 for oid in sorted(nodeids):
1569 path = self.NODE_ROOT + '/' + oid
1570 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001571 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001572 data['_oid'] = oid
1573 try:
1574 lockfiles = self.client.get_children(path + '/lock')
1575 except kazoo.exceptions.NoNodeError:
1576 lockfiles = []
1577 if lockfiles:
1578 data['_lock'] = True
1579 else:
1580 data['_lock'] = False
1581 nodes.append(data)
1582 return nodes
1583
James E. Blaira38c28e2017-01-04 10:33:20 -08001584 def makeNode(self, request_id, node_type):
1585 now = time.time()
1586 path = '/nodepool/nodes/'
1587 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001588 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001589 provider='test-provider',
1590 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001591 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001592 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001593 public_ipv4='127.0.0.1',
1594 private_ipv4=None,
1595 public_ipv6=None,
1596 allocated_to=request_id,
1597 state='ready',
1598 state_time=now,
1599 created_time=now,
1600 updated_time=now,
1601 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001602 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001603 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001604 if 'fakeuser' in node_type:
1605 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001606 if 'windows' in node_type:
1607 data['connection_type'] = 'winrm'
1608
Clint Byrumf322fe22017-05-10 20:53:12 -07001609 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001610 path = self.client.create(path, data,
1611 makepath=True,
1612 sequence=True)
1613 nodeid = path.split("/")[-1]
1614 return nodeid
1615
James E. Blair6ab79e02017-01-06 10:10:17 -08001616 def addFailRequest(self, request):
1617 self.fail_requests.add(request['_oid'])
1618
James E. Blairdce6cea2016-12-20 16:45:32 -08001619 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001620 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001621 return
1622 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001623 oid = request['_oid']
1624 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001625
James E. Blair6ab79e02017-01-06 10:10:17 -08001626 if oid in self.fail_requests:
1627 request['state'] = 'failed'
1628 else:
1629 request['state'] = 'fulfilled'
1630 nodes = []
1631 for node in request['node_types']:
1632 nodeid = self.makeNode(oid, node)
1633 nodes.append(nodeid)
1634 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001635
James E. Blaira38c28e2017-01-04 10:33:20 -08001636 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001637 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001638 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001639 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001640 try:
1641 self.client.set(path, data)
1642 except kazoo.exceptions.NoNodeError:
1643 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001644
1645
James E. Blair498059b2016-12-20 13:50:13 -08001646class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001647 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001648 super(ChrootedKazooFixture, self).__init__()
1649
1650 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1651 if ':' in zk_host:
1652 host, port = zk_host.split(':')
1653 else:
1654 host = zk_host
1655 port = None
1656
1657 self.zookeeper_host = host
1658
1659 if not port:
1660 self.zookeeper_port = 2181
1661 else:
1662 self.zookeeper_port = int(port)
1663
Clark Boylan621ec9a2017-04-07 17:41:33 -07001664 self.test_id = test_id
1665
James E. Blair498059b2016-12-20 13:50:13 -08001666 def _setUp(self):
1667 # Make sure the test chroot paths do not conflict
1668 random_bits = ''.join(random.choice(string.ascii_lowercase +
1669 string.ascii_uppercase)
1670 for x in range(8))
1671
Clark Boylan621ec9a2017-04-07 17:41:33 -07001672 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001673 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1674
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001675 self.addCleanup(self._cleanup)
1676
James E. Blair498059b2016-12-20 13:50:13 -08001677 # Ensure the chroot path exists and clean up any pre-existing znodes.
1678 _tmp_client = kazoo.client.KazooClient(
1679 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1680 _tmp_client.start()
1681
1682 if _tmp_client.exists(self.zookeeper_chroot):
1683 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1684
1685 _tmp_client.ensure_path(self.zookeeper_chroot)
1686 _tmp_client.stop()
1687 _tmp_client.close()
1688
James E. Blair498059b2016-12-20 13:50:13 -08001689 def _cleanup(self):
1690 '''Remove the chroot path.'''
1691 # Need a non-chroot'ed client to remove the chroot path
1692 _tmp_client = kazoo.client.KazooClient(
1693 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1694 _tmp_client.start()
1695 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1696 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001697 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001698
1699
Joshua Heskethd78b4482015-09-14 16:56:34 -06001700class MySQLSchemaFixture(fixtures.Fixture):
1701 def setUp(self):
1702 super(MySQLSchemaFixture, self).setUp()
1703
1704 random_bits = ''.join(random.choice(string.ascii_lowercase +
1705 string.ascii_uppercase)
1706 for x in range(8))
1707 self.name = '%s_%s' % (random_bits, os.getpid())
1708 self.passwd = uuid.uuid4().hex
1709 db = pymysql.connect(host="localhost",
1710 user="openstack_citest",
1711 passwd="openstack_citest",
1712 db="openstack_citest")
1713 cur = db.cursor()
1714 cur.execute("create database %s" % self.name)
1715 cur.execute(
1716 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1717 (self.name, self.name, self.passwd))
1718 cur.execute("flush privileges")
1719
1720 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1721 self.passwd,
1722 self.name)
1723 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1724 self.addCleanup(self.cleanup)
1725
1726 def cleanup(self):
1727 db = pymysql.connect(host="localhost",
1728 user="openstack_citest",
1729 passwd="openstack_citest",
1730 db="openstack_citest")
1731 cur = db.cursor()
1732 cur.execute("drop database %s" % self.name)
1733 cur.execute("drop user '%s'@'localhost'" % self.name)
1734 cur.execute("flush privileges")
1735
1736
Maru Newby3fe5f852015-01-13 04:22:14 +00001737class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001738 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001739 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001740
James E. Blair1c236df2017-02-01 14:07:24 -08001741 def attachLogs(self, *args):
1742 def reader():
1743 self._log_stream.seek(0)
1744 while True:
1745 x = self._log_stream.read(4096)
1746 if not x:
1747 break
1748 yield x.encode('utf8')
1749 content = testtools.content.content_from_reader(
1750 reader,
1751 testtools.content_type.UTF8_TEXT,
1752 False)
1753 self.addDetail('logging', content)
1754
Clark Boylanb640e052014-04-03 16:41:46 -07001755 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001756 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001757 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1758 try:
1759 test_timeout = int(test_timeout)
1760 except ValueError:
1761 # If timeout value is invalid do not set a timeout.
1762 test_timeout = 0
1763 if test_timeout > 0:
1764 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1765
1766 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1767 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1768 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1769 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1770 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1771 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1772 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1773 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1774 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1775 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001776 self._log_stream = StringIO()
1777 self.addOnException(self.attachLogs)
1778 else:
1779 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001780
James E. Blair73b41772017-05-22 13:22:55 -07001781 # NOTE(jeblair): this is temporary extra debugging to try to
1782 # track down a possible leak.
1783 orig_git_repo_init = git.Repo.__init__
1784
1785 def git_repo_init(myself, *args, **kw):
1786 orig_git_repo_init(myself, *args, **kw)
1787 self.log.debug("Created git repo 0x%x %s" %
1788 (id(myself), repr(myself)))
1789
1790 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1791 git_repo_init))
1792
James E. Blair1c236df2017-02-01 14:07:24 -08001793 handler = logging.StreamHandler(self._log_stream)
1794 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1795 '%(levelname)-8s %(message)s')
1796 handler.setFormatter(formatter)
1797
1798 logger = logging.getLogger()
1799 logger.setLevel(logging.DEBUG)
1800 logger.addHandler(handler)
1801
Clark Boylan3410d532017-04-25 12:35:29 -07001802 # Make sure we don't carry old handlers around in process state
1803 # which slows down test runs
1804 self.addCleanup(logger.removeHandler, handler)
1805 self.addCleanup(handler.close)
1806 self.addCleanup(handler.flush)
1807
James E. Blair1c236df2017-02-01 14:07:24 -08001808 # NOTE(notmorgan): Extract logging overrides for specific
1809 # libraries from the OS_LOG_DEFAULTS env and create loggers
1810 # for each. This is used to limit the output during test runs
1811 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001812 log_defaults_from_env = os.environ.get(
1813 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001814 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001815
James E. Blairdce6cea2016-12-20 16:45:32 -08001816 if log_defaults_from_env:
1817 for default in log_defaults_from_env.split(','):
1818 try:
1819 name, level_str = default.split('=', 1)
1820 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001821 logger = logging.getLogger(name)
1822 logger.setLevel(level)
1823 logger.addHandler(handler)
1824 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001825 except ValueError:
1826 # NOTE(notmorgan): Invalid format of the log default,
1827 # skip and don't try and apply a logger for the
1828 # specified module
1829 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001830
Maru Newby3fe5f852015-01-13 04:22:14 +00001831
1832class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001833 """A test case with a functioning Zuul.
1834
1835 The following class variables are used during test setup and can
1836 be overidden by subclasses but are effectively read-only once a
1837 test method starts running:
1838
1839 :cvar str config_file: This points to the main zuul config file
1840 within the fixtures directory. Subclasses may override this
1841 to obtain a different behavior.
1842
1843 :cvar str tenant_config_file: This is the tenant config file
1844 (which specifies from what git repos the configuration should
1845 be loaded). It defaults to the value specified in
1846 `config_file` but can be overidden by subclasses to obtain a
1847 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001848 configuration. See also the :py:func:`simple_layout`
1849 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001850
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001851 :cvar bool create_project_keys: Indicates whether Zuul should
1852 auto-generate keys for each project, or whether the test
1853 infrastructure should insert dummy keys to save time during
1854 startup. Defaults to False.
1855
James E. Blaire7b99a02016-08-05 14:27:34 -07001856 The following are instance variables that are useful within test
1857 methods:
1858
1859 :ivar FakeGerritConnection fake_<connection>:
1860 A :py:class:`~tests.base.FakeGerritConnection` will be
1861 instantiated for each connection present in the config file
1862 and stored here. For instance, `fake_gerrit` will hold the
1863 FakeGerritConnection object for a connection named `gerrit`.
1864
1865 :ivar FakeGearmanServer gearman_server: An instance of
1866 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1867 server that all of the Zuul components in this test use to
1868 communicate with each other.
1869
Paul Belanger174a8272017-03-14 13:20:10 -04001870 :ivar RecordingExecutorServer executor_server: An instance of
1871 :py:class:`~tests.base.RecordingExecutorServer` which is the
1872 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001873
1874 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1875 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001876 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001877 list upon completion.
1878
1879 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1880 objects representing completed builds. They are appended to
1881 the list in the order they complete.
1882
1883 """
1884
James E. Blair83005782015-12-11 14:46:03 -08001885 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001886 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001887 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001888 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001889
1890 def _startMerger(self):
1891 self.merge_server = zuul.merger.server.MergeServer(self.config,
1892 self.connections)
1893 self.merge_server.start()
1894
Maru Newby3fe5f852015-01-13 04:22:14 +00001895 def setUp(self):
1896 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001897
1898 self.setupZK()
1899
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001900 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001901 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001902 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1903 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001904 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001905 tmp_root = tempfile.mkdtemp(
1906 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001907 self.test_root = os.path.join(tmp_root, "zuul-test")
1908 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001909 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001910 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001911 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001912 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1913 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001914
1915 if os.path.exists(self.test_root):
1916 shutil.rmtree(self.test_root)
1917 os.makedirs(self.test_root)
1918 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001919 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001920 os.makedirs(self.merger_state_root)
1921 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001922
1923 # Make per test copy of Configuration.
1924 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001925 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1926 if not os.path.exists(self.private_key_file):
1927 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1928 shutil.copy(src_private_key_file, self.private_key_file)
1929 shutil.copy('{}.pub'.format(src_private_key_file),
1930 '{}.pub'.format(self.private_key_file))
1931 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001932 self.config.set('scheduler', 'tenant_config',
1933 os.path.join(
1934 FIXTURE_DIR,
1935 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001936 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001937 self.config.set(
1938 'scheduler', 'command_socket',
1939 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001940 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001941 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001942 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001943 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001944 self.config.set(
1945 'executor', 'command_socket',
1946 os.path.join(self.test_root, 'executor.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001947
Clark Boylanb640e052014-04-03 16:41:46 -07001948 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001949 if self.config.has_section('statsd'):
1950 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001951 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001952
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001953 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001954
1955 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001956 self.log.info("Gearman server on port %s" %
1957 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001958 if self.use_ssl:
1959 self.log.info('SSL enabled for gearman')
1960 self.config.set(
1961 'gearman', 'ssl_ca',
1962 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1963 self.config.set(
1964 'gearman', 'ssl_cert',
1965 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1966 self.config.set(
1967 'gearman', 'ssl_key',
1968 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001969
James E. Blaire511d2f2016-12-08 15:22:26 -08001970 gerritsource.GerritSource.replication_timeout = 1.5
1971 gerritsource.GerritSource.replication_retry_interval = 0.5
1972 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001973
Joshua Hesketh352264b2015-08-11 23:42:08 +10001974 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07001975 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07001976
Jan Hruban7083edd2015-08-21 14:00:54 +02001977 self.webapp = zuul.webapp.WebApp(
1978 self.sched, port=0, listen_address='127.0.0.1')
1979
Jan Hruban6b71aff2015-10-22 16:58:08 +02001980 self.event_queues = [
1981 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001982 self.sched.trigger_event_queue,
1983 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001984 ]
1985
James E. Blairfef78942016-03-11 16:28:56 -08001986 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001987 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001988
Paul Belanger174a8272017-03-14 13:20:10 -04001989 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001990 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001991 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001992 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001993 _test_root=self.test_root,
1994 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001995 self.executor_server.start()
1996 self.history = self.executor_server.build_history
1997 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001998
Paul Belanger174a8272017-03-14 13:20:10 -04001999 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002000 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002001 self.merge_client = zuul.merger.client.MergeClient(
2002 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002003 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002004 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002005 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002006
James E. Blair0d5a36e2017-02-21 10:53:44 -05002007 self.fake_nodepool = FakeNodepool(
2008 self.zk_chroot_fixture.zookeeper_host,
2009 self.zk_chroot_fixture.zookeeper_port,
2010 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002011
Paul Belanger174a8272017-03-14 13:20:10 -04002012 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002013 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002014 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002015 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002016
Clark Boylanb640e052014-04-03 16:41:46 -07002017 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002018 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002019 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002020 # Cleanups are run in reverse order
2021 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002022 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002023 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002024
James E. Blairb9c0d772017-03-03 14:34:49 -08002025 self.sched.reconfigure(self.config)
2026 self.sched.resume()
2027
Tobias Henkel7df274b2017-05-26 17:41:11 +02002028 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002029 # Set up gerrit related fakes
2030 # Set a changes database so multiple FakeGerrit's can report back to
2031 # a virtual canonical database given by the configured hostname
2032 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002033 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002034
2035 def getGerritConnection(driver, name, config):
2036 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2037 con = FakeGerritConnection(driver, name, config,
2038 changes_db=db,
2039 upstream_root=self.upstream_root)
2040 self.event_queues.append(con.event_queue)
2041 setattr(self, 'fake_' + name, con)
2042 return con
2043
2044 self.useFixture(fixtures.MonkeyPatch(
2045 'zuul.driver.gerrit.GerritDriver.getConnection',
2046 getGerritConnection))
2047
Gregory Haynes4fc12542015-04-22 20:38:06 -07002048 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002049 server = config.get('server', 'github.com')
2050 db = self.github_changes_dbs.setdefault(server, {})
Gregory Haynes4fc12542015-04-22 20:38:06 -07002051 con = FakeGithubConnection(driver, name, config,
James E. Blair6bacffb2018-01-05 13:45:25 -08002052 changes_db=db,
Gregory Haynes4fc12542015-04-22 20:38:06 -07002053 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002054 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002055 setattr(self, 'fake_' + name, con)
2056 return con
2057
2058 self.useFixture(fixtures.MonkeyPatch(
2059 'zuul.driver.github.GithubDriver.getConnection',
2060 getGithubConnection))
2061
James E. Blaire511d2f2016-12-08 15:22:26 -08002062 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002063 # TODO(jhesketh): This should come from lib.connections for better
2064 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002065 # Register connections from the config
2066 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002067
Joshua Hesketh352264b2015-08-11 23:42:08 +10002068 def FakeSMTPFactory(*args, **kw):
2069 args = [self.smtp_messages] + list(args)
2070 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002071
Joshua Hesketh352264b2015-08-11 23:42:08 +10002072 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002073
James E. Blaire511d2f2016-12-08 15:22:26 -08002074 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002075 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002076 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002077
James E. Blair83005782015-12-11 14:46:03 -08002078 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002079 # This creates the per-test configuration object. It can be
2080 # overriden by subclasses, but should not need to be since it
2081 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002082 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002083 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002084
James E. Blair39840362017-06-23 20:34:02 +01002085 sections = ['zuul', 'scheduler', 'executor', 'merger']
2086 for section in sections:
2087 if not self.config.has_section(section):
2088 self.config.add_section(section)
2089
James E. Blair06cc3922017-04-19 10:08:10 -07002090 if not self.setupSimpleLayout():
2091 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002092 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002093 self.tenant_config_file)
2094 git_path = os.path.join(
2095 os.path.dirname(
2096 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2097 'git')
2098 if os.path.exists(git_path):
2099 for reponame in os.listdir(git_path):
2100 project = reponame.replace('_', '/')
2101 self.copyDirToRepo(project,
2102 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002103 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002104 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002105 self.setupAllProjectKeys()
2106
James E. Blair06cc3922017-04-19 10:08:10 -07002107 def setupSimpleLayout(self):
2108 # If the test method has been decorated with a simple_layout,
2109 # use that instead of the class tenant_config_file. Set up a
2110 # single config-project with the specified layout, and
2111 # initialize repos for all of the 'project' entries which
2112 # appear in the layout.
2113 test_name = self.id().split('.')[-1]
2114 test = getattr(self, test_name)
2115 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002116 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002117 else:
2118 return False
2119
James E. Blairb70e55a2017-04-19 12:57:02 -07002120 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002121 path = os.path.join(FIXTURE_DIR, path)
2122 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002123 data = f.read()
2124 layout = yaml.safe_load(data)
2125 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002126 untrusted_projects = []
2127 for item in layout:
2128 if 'project' in item:
2129 name = item['project']['name']
2130 untrusted_projects.append(name)
2131 self.init_repo(name)
2132 self.addCommitToRepo(name, 'initial commit',
2133 files={'README': ''},
2134 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002135 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002136 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002137 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002138 for fn in zuul.configloader.as_list(
2139 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002140 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002141 for fn in zuul.configloader.as_list(
2142 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002143 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002144
2145 root = os.path.join(self.test_root, "config")
2146 if not os.path.exists(root):
2147 os.makedirs(root)
2148 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2149 config = [{'tenant':
2150 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002151 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002152 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002153 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002154 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002155 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002156 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002157 os.path.join(FIXTURE_DIR, f.name))
2158
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002159 self.init_repo('org/common-config')
2160 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002161 files, branch='master', tag='init')
2162
2163 return True
2164
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002165 def setupAllProjectKeys(self):
2166 if self.create_project_keys:
2167 return
2168
James E. Blair39840362017-06-23 20:34:02 +01002169 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002170 with open(os.path.join(FIXTURE_DIR, path)) as f:
2171 tenant_config = yaml.safe_load(f.read())
2172 for tenant in tenant_config:
2173 sources = tenant['tenant']['source']
2174 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002175 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002176 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002177 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002178 self.setupProjectKeys(source, project)
2179
2180 def setupProjectKeys(self, source, project):
2181 # Make sure we set up an RSA key for the project so that we
2182 # don't spend time generating one:
2183
James E. Blair6459db12017-06-29 14:57:20 -07002184 if isinstance(project, dict):
2185 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002186 key_root = os.path.join(self.state_root, 'keys')
2187 if not os.path.isdir(key_root):
2188 os.mkdir(key_root, 0o700)
2189 private_key_file = os.path.join(key_root, source, project + '.pem')
2190 private_key_dir = os.path.dirname(private_key_file)
2191 self.log.debug("Installing test keys for project %s at %s" % (
2192 project, private_key_file))
2193 if not os.path.isdir(private_key_dir):
2194 os.makedirs(private_key_dir)
2195 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2196 with open(private_key_file, 'w') as o:
2197 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002198
James E. Blair498059b2016-12-20 13:50:13 -08002199 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002200 self.zk_chroot_fixture = self.useFixture(
2201 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002202 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002203 self.zk_chroot_fixture.zookeeper_host,
2204 self.zk_chroot_fixture.zookeeper_port,
2205 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002206
James E. Blair96c6bf82016-01-15 16:20:40 -08002207 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002208 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002209
2210 files = {}
2211 for (dirpath, dirnames, filenames) in os.walk(source_path):
2212 for filename in filenames:
2213 test_tree_filepath = os.path.join(dirpath, filename)
2214 common_path = os.path.commonprefix([test_tree_filepath,
2215 source_path])
2216 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2217 with open(test_tree_filepath, 'r') as f:
2218 content = f.read()
2219 files[relative_filepath] = content
2220 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002221 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002222
James E. Blaire18d4602017-01-05 11:17:28 -08002223 def assertNodepoolState(self):
2224 # Make sure that there are no pending requests
2225
2226 requests = self.fake_nodepool.getNodeRequests()
2227 self.assertEqual(len(requests), 0)
2228
2229 nodes = self.fake_nodepool.getNodes()
2230 for node in nodes:
2231 self.assertFalse(node['_lock'], "Node %s is locked" %
2232 (node['_oid'],))
2233
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002234 def assertNoGeneratedKeys(self):
2235 # Make sure that Zuul did not generate any project keys
2236 # (unless it was supposed to).
2237
2238 if self.create_project_keys:
2239 return
2240
2241 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2242 test_key = i.read()
2243
2244 key_root = os.path.join(self.state_root, 'keys')
2245 for root, dirname, files in os.walk(key_root):
2246 for fn in files:
2247 with open(os.path.join(root, fn)) as f:
2248 self.assertEqual(test_key, f.read())
2249
Clark Boylanb640e052014-04-03 16:41:46 -07002250 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002251 self.log.debug("Assert final state")
2252 # Make sure no jobs are running
2253 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002254 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002255 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002256 gc.collect()
2257 for obj in gc.get_objects():
2258 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002259 self.log.debug("Leaked git repo object: 0x%x %s" %
2260 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002261 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002262 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002263 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002264 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002265 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002266 for tenant in self.sched.abide.tenants.values():
2267 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002268 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002269 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002270
2271 def shutdown(self):
2272 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002273 self.executor_server.hold_jobs_in_build = False
2274 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002275 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002276 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002277 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002278 self.sched.stop()
2279 self.sched.join()
2280 self.statsd.stop()
2281 self.statsd.join()
2282 self.webapp.stop()
2283 self.webapp.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002284 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002285 self.fake_nodepool.stop()
2286 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002287 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002288 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002289 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002290 # Further the pydevd threads also need to be whitelisted so debugging
2291 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002292 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002293 'pydevd.CommandThread',
2294 'pydevd.Reader',
2295 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002296 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002297 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002298 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002299 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002300 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002301 log_str = ""
2302 for thread_id, stack_frame in sys._current_frames().items():
2303 log_str += "Thread: %s\n" % thread_id
2304 log_str += "".join(traceback.format_stack(stack_frame))
2305 self.log.debug(log_str)
2306 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002307
James E. Blaira002b032017-04-18 10:35:48 -07002308 def assertCleanShutdown(self):
2309 pass
2310
James E. Blairc4ba97a2017-04-19 16:26:24 -07002311 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002312 parts = project.split('/')
2313 path = os.path.join(self.upstream_root, *parts[:-1])
2314 if not os.path.exists(path):
2315 os.makedirs(path)
2316 path = os.path.join(self.upstream_root, project)
2317 repo = git.Repo.init(path)
2318
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002319 with repo.config_writer() as config_writer:
2320 config_writer.set_value('user', 'email', 'user@example.com')
2321 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002322
Clark Boylanb640e052014-04-03 16:41:46 -07002323 repo.index.commit('initial commit')
2324 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002325 if tag:
2326 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002327
James E. Blair97d902e2014-08-21 13:25:56 -07002328 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002329 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002330 repo.git.clean('-x', '-f', '-d')
2331
James E. Blair97d902e2014-08-21 13:25:56 -07002332 def create_branch(self, project, branch):
2333 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002334 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002335 fn = os.path.join(path, 'README')
2336
2337 branch_head = repo.create_head(branch)
2338 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002339 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002340 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002341 f.close()
2342 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002343 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002344
James E. Blair97d902e2014-08-21 13:25:56 -07002345 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002346 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002347 repo.git.clean('-x', '-f', '-d')
2348
Sachi King9f16d522016-03-16 12:20:45 +11002349 def create_commit(self, project):
2350 path = os.path.join(self.upstream_root, project)
2351 repo = git.Repo(path)
2352 repo.head.reference = repo.heads['master']
2353 file_name = os.path.join(path, 'README')
2354 with open(file_name, 'a') as f:
2355 f.write('creating fake commit\n')
2356 repo.index.add([file_name])
2357 commit = repo.index.commit('Creating a fake commit')
2358 return commit.hexsha
2359
James E. Blairf4a5f022017-04-18 14:01:10 -07002360 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002361 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002362 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002363 while len(self.builds):
2364 self.release(self.builds[0])
2365 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002366 i += 1
2367 if count is not None and i >= count:
2368 break
James E. Blairb8c16472015-05-05 14:55:26 -07002369
James E. Blairdf25ddc2017-07-08 07:57:09 -07002370 def getSortedBuilds(self):
2371 "Return the list of currently running builds sorted by name"
2372
2373 return sorted(self.builds, key=lambda x: x.name)
2374
Clark Boylanb640e052014-04-03 16:41:46 -07002375 def release(self, job):
2376 if isinstance(job, FakeBuild):
2377 job.release()
2378 else:
2379 job.waiting = False
2380 self.log.debug("Queued job %s released" % job.unique)
2381 self.gearman_server.wakeConnections()
2382
2383 def getParameter(self, job, name):
2384 if isinstance(job, FakeBuild):
2385 return job.parameters[name]
2386 else:
2387 parameters = json.loads(job.arguments)
2388 return parameters[name]
2389
Clark Boylanb640e052014-04-03 16:41:46 -07002390 def haveAllBuildsReported(self):
2391 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002392 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002393 return False
2394 # Find out if every build that the worker has completed has been
2395 # reported back to Zuul. If it hasn't then that means a Gearman
2396 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002397 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002398 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002399 if not zbuild:
2400 # It has already been reported
2401 continue
2402 # It hasn't been reported yet.
2403 return False
2404 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002405 worker = self.executor_server.executor_worker
2406 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002407 if connection.state == 'GRAB_WAIT':
2408 return False
2409 return True
2410
2411 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002412 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002413 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002414 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002415 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002416 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002417 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002418 for j in conn.related_jobs.values():
2419 if j.unique == build.uuid:
2420 client_job = j
2421 break
2422 if not client_job:
2423 self.log.debug("%s is not known to the gearman client" %
2424 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002425 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002426 if not client_job.handle:
2427 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002428 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002429 server_job = self.gearman_server.jobs.get(client_job.handle)
2430 if not server_job:
2431 self.log.debug("%s is not known to the gearman server" %
2432 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002433 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002434 if not hasattr(server_job, 'waiting'):
2435 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002436 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002437 if server_job.waiting:
2438 continue
James E. Blair17302972016-08-10 16:11:42 -07002439 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002440 self.log.debug("%s has not reported start" % build)
2441 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002442 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002443 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002444 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002445 if worker_build:
2446 if worker_build.isWaiting():
2447 continue
2448 else:
2449 self.log.debug("%s is running" % worker_build)
2450 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002451 else:
James E. Blair962220f2016-08-03 11:22:38 -07002452 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002453 return False
James E. Blaira002b032017-04-18 10:35:48 -07002454 for (build_uuid, job_worker) in \
2455 self.executor_server.job_workers.items():
2456 if build_uuid not in seen_builds:
2457 self.log.debug("%s is not finalized" % build_uuid)
2458 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002459 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002460
James E. Blairdce6cea2016-12-20 16:45:32 -08002461 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002462 if self.fake_nodepool.paused:
2463 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002464 if self.sched.nodepool.requests:
2465 return False
2466 return True
2467
James E. Blaira615c362017-10-02 17:34:42 -07002468 def areAllMergeJobsWaiting(self):
2469 for client_job in list(self.merge_client.jobs):
2470 if not client_job.handle:
2471 self.log.debug("%s has no handle" % client_job)
2472 return False
2473 server_job = self.gearman_server.jobs.get(client_job.handle)
2474 if not server_job:
2475 self.log.debug("%s is not known to the gearman server" %
2476 client_job)
2477 return False
2478 if not hasattr(server_job, 'waiting'):
2479 self.log.debug("%s is being enqueued" % server_job)
2480 return False
2481 if server_job.waiting:
2482 self.log.debug("%s is waiting" % server_job)
2483 continue
2484 self.log.debug("%s is not waiting" % server_job)
2485 return False
2486 return True
2487
Jan Hruban6b71aff2015-10-22 16:58:08 +02002488 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002489 for event_queue in self.event_queues:
2490 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002491
2492 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002493 for event_queue in self.event_queues:
2494 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002495
Clark Boylanb640e052014-04-03 16:41:46 -07002496 def waitUntilSettled(self):
2497 self.log.debug("Waiting until settled...")
2498 start = time.time()
2499 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002500 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002501 self.log.error("Timeout waiting for Zuul to settle")
2502 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002503 for event_queue in self.event_queues:
2504 self.log.error(" %s: %s" %
2505 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002506 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002507 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002508 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002509 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002510 self.log.error("All requests completed: %s" %
2511 (self.areAllNodeRequestsComplete(),))
2512 self.log.error("Merge client jobs: %s" %
2513 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002514 raise Exception("Timeout waiting for Zuul to settle")
2515 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002516
Paul Belanger174a8272017-03-14 13:20:10 -04002517 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002518 # have all build states propogated to zuul?
2519 if self.haveAllBuildsReported():
2520 # Join ensures that the queue is empty _and_ events have been
2521 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002522 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002523 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002524 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002525 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002526 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002527 self.areAllNodeRequestsComplete() and
2528 all(self.eventQueuesEmpty())):
2529 # The queue empty check is placed at the end to
2530 # ensure that if a component adds an event between
2531 # when locked the run handler and checked that the
2532 # components were stable, we don't erroneously
2533 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002534 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002535 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002536 self.log.debug("...settled.")
2537 return
2538 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002539 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002540 self.sched.wake_event.wait(0.1)
2541
2542 def countJobResults(self, jobs, result):
2543 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002544 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002545
Monty Taylor0d926122017-05-24 08:07:56 -05002546 def getBuildByName(self, name):
2547 for build in self.builds:
2548 if build.name == name:
2549 return build
2550 raise Exception("Unable to find build %s" % name)
2551
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002552 def assertJobNotInHistory(self, name, project=None):
2553 for job in self.history:
2554 if (project is None or
2555 job.parameters['zuul']['project']['name'] == project):
2556 self.assertNotEqual(job.name, name,
2557 'Job %s found in history' % name)
2558
James E. Blair96c6bf82016-01-15 16:20:40 -08002559 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002560 for job in self.history:
2561 if (job.name == name and
2562 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002563 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002564 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002565 raise Exception("Unable to find job %s in history" % name)
2566
2567 def assertEmptyQueues(self):
2568 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002569 for tenant in self.sched.abide.tenants.values():
2570 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002571 for pipeline_queue in pipeline.queues:
2572 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002573 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002574 pipeline.name, pipeline_queue.name,
2575 pipeline_queue.queue))
2576 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002577 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002578
2579 def assertReportedStat(self, key, value=None, kind=None):
2580 start = time.time()
2581 while time.time() < (start + 5):
2582 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002583 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002584 if key == k:
2585 if value is None and kind is None:
2586 return
2587 elif value:
2588 if value == v:
2589 return
2590 elif kind:
2591 if v.endswith('|' + kind):
2592 return
2593 time.sleep(0.1)
2594
Clark Boylanb640e052014-04-03 16:41:46 -07002595 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002596
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002597 def assertBuilds(self, builds):
2598 """Assert that the running builds are as described.
2599
2600 The list of running builds is examined and must match exactly
2601 the list of builds described by the input.
2602
2603 :arg list builds: A list of dictionaries. Each item in the
2604 list must match the corresponding build in the build
2605 history, and each element of the dictionary must match the
2606 corresponding attribute of the build.
2607
2608 """
James E. Blair3158e282016-08-19 09:34:11 -07002609 try:
2610 self.assertEqual(len(self.builds), len(builds))
2611 for i, d in enumerate(builds):
2612 for k, v in d.items():
2613 self.assertEqual(
2614 getattr(self.builds[i], k), v,
2615 "Element %i in builds does not match" % (i,))
2616 except Exception:
2617 for build in self.builds:
2618 self.log.error("Running build: %s" % build)
2619 else:
2620 self.log.error("No running builds")
2621 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002622
James E. Blairb536ecc2016-08-31 10:11:42 -07002623 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002624 """Assert that the completed builds are as described.
2625
2626 The list of completed builds is examined and must match
2627 exactly the list of builds described by the input.
2628
2629 :arg list history: A list of dictionaries. Each item in the
2630 list must match the corresponding build in the build
2631 history, and each element of the dictionary must match the
2632 corresponding attribute of the build.
2633
James E. Blairb536ecc2016-08-31 10:11:42 -07002634 :arg bool ordered: If true, the history must match the order
2635 supplied, if false, the builds are permitted to have
2636 arrived in any order.
2637
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002638 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002639 def matches(history_item, item):
2640 for k, v in item.items():
2641 if getattr(history_item, k) != v:
2642 return False
2643 return True
James E. Blair3158e282016-08-19 09:34:11 -07002644 try:
2645 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002646 if ordered:
2647 for i, d in enumerate(history):
2648 if not matches(self.history[i], d):
2649 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002650 "Element %i in history does not match %s" %
2651 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002652 else:
2653 unseen = self.history[:]
2654 for i, d in enumerate(history):
2655 found = False
2656 for unseen_item in unseen:
2657 if matches(unseen_item, d):
2658 found = True
2659 unseen.remove(unseen_item)
2660 break
2661 if not found:
2662 raise Exception("No match found for element %i "
2663 "in history" % (i,))
2664 if unseen:
2665 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002666 except Exception:
2667 for build in self.history:
2668 self.log.error("Completed build: %s" % build)
2669 else:
2670 self.log.error("No completed builds")
2671 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002672
James E. Blair6ac368c2016-12-22 18:07:20 -08002673 def printHistory(self):
2674 """Log the build history.
2675
2676 This can be useful during tests to summarize what jobs have
2677 completed.
2678
2679 """
2680 self.log.debug("Build history:")
2681 for build in self.history:
2682 self.log.debug(build)
2683
James E. Blair59fdbac2015-12-07 17:08:06 -08002684 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002685 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2686
James E. Blair9ea70072017-04-19 16:05:30 -07002687 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002688 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002689 if not os.path.exists(root):
2690 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002691 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2692 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002693- tenant:
2694 name: openstack
2695 source:
2696 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002697 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002698 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002699 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002700 - org/project
2701 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002702 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002703 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002704 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002705 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002706 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002707
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002708 def addTagToRepo(self, project, name, sha):
2709 path = os.path.join(self.upstream_root, project)
2710 repo = git.Repo(path)
2711 repo.git.tag(name, sha)
2712
2713 def delTagFromRepo(self, project, name):
2714 path = os.path.join(self.upstream_root, project)
2715 repo = git.Repo(path)
2716 repo.git.tag('-d', name)
2717
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002718 def addCommitToRepo(self, project, message, files,
2719 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002720 path = os.path.join(self.upstream_root, project)
2721 repo = git.Repo(path)
2722 repo.head.reference = branch
2723 zuul.merger.merger.reset_repo_to_head(repo)
2724 for fn, content in files.items():
2725 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002726 try:
2727 os.makedirs(os.path.dirname(fn))
2728 except OSError:
2729 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002730 with open(fn, 'w') as f:
2731 f.write(content)
2732 repo.index.add([fn])
2733 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002734 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002735 repo.heads[branch].commit = commit
2736 repo.head.reference = branch
2737 repo.git.clean('-x', '-f', '-d')
2738 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002739 if tag:
2740 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002741 return before
2742
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002743 def commitConfigUpdate(self, project_name, source_name):
2744 """Commit an update to zuul.yaml
2745
2746 This overwrites the zuul.yaml in the specificed project with
2747 the contents specified.
2748
2749 :arg str project_name: The name of the project containing
2750 zuul.yaml (e.g., common-config)
2751
2752 :arg str source_name: The path to the file (underneath the
2753 test fixture directory) whose contents should be used to
2754 replace zuul.yaml.
2755 """
2756
2757 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002758 files = {}
2759 with open(source_path, 'r') as f:
2760 data = f.read()
2761 layout = yaml.safe_load(data)
2762 files['zuul.yaml'] = data
2763 for item in layout:
2764 if 'job' in item:
2765 jobname = item['job']['name']
2766 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002767 before = self.addCommitToRepo(
2768 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002769 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002770 return before
2771
Clint Byrum627ba362017-08-14 13:20:40 -07002772 def newTenantConfig(self, source_name):
2773 """ Use this to update the tenant config file in tests
2774
2775 This will update self.tenant_config_file to point to a temporary file
2776 for the duration of this particular test. The content of that file will
2777 be taken from FIXTURE_DIR/source_name
2778
2779 After the test the original value of self.tenant_config_file will be
2780 restored.
2781
2782 :arg str source_name: The path of the file under
2783 FIXTURE_DIR that will be used to populate the new tenant
2784 config file.
2785 """
2786 source_path = os.path.join(FIXTURE_DIR, source_name)
2787 orig_tenant_config_file = self.tenant_config_file
2788 with tempfile.NamedTemporaryFile(
2789 delete=False, mode='wb') as new_tenant_config:
2790 self.tenant_config_file = new_tenant_config.name
2791 with open(source_path, mode='rb') as source_tenant_config:
2792 new_tenant_config.write(source_tenant_config.read())
2793 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2794 self.setupAllProjectKeys()
2795 self.log.debug(
2796 'tenant_config_file = {}'.format(self.tenant_config_file))
2797
2798 def _restoreTenantConfig():
2799 self.log.debug(
2800 'restoring tenant_config_file = {}'.format(
2801 orig_tenant_config_file))
2802 os.unlink(self.tenant_config_file)
2803 self.tenant_config_file = orig_tenant_config_file
2804 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2805 self.addCleanup(_restoreTenantConfig)
2806
James E. Blair7fc8daa2016-08-08 15:37:15 -07002807 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002808
James E. Blair7fc8daa2016-08-08 15:37:15 -07002809 """Inject a Fake (Gerrit) event.
2810
2811 This method accepts a JSON-encoded event and simulates Zuul
2812 having received it from Gerrit. It could (and should)
2813 eventually apply to any connection type, but is currently only
2814 used with Gerrit connections. The name of the connection is
2815 used to look up the corresponding server, and the event is
2816 simulated as having been received by all Zuul connections
2817 attached to that server. So if two Gerrit connections in Zuul
2818 are connected to the same Gerrit server, and you invoke this
2819 method specifying the name of one of them, the event will be
2820 received by both.
2821
2822 .. note::
2823
2824 "self.fake_gerrit.addEvent" calls should be migrated to
2825 this method.
2826
2827 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002828 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002829 :arg str event: The JSON-encoded event.
2830
2831 """
2832 specified_conn = self.connections.connections[connection]
2833 for conn in self.connections.connections.values():
2834 if (isinstance(conn, specified_conn.__class__) and
2835 specified_conn.server == conn.server):
2836 conn.addEvent(event)
2837
James E. Blaird8af5422017-05-24 13:59:40 -07002838 def getUpstreamRepos(self, projects):
2839 """Return upstream git repo objects for the listed projects
2840
2841 :arg list projects: A list of strings, each the canonical name
2842 of a project.
2843
2844 :returns: A dictionary of {name: repo} for every listed
2845 project.
2846 :rtype: dict
2847
2848 """
2849
2850 repos = {}
2851 for project in projects:
2852 # FIXME(jeblair): the upstream root does not yet have a
2853 # hostname component; that needs to be added, and this
2854 # line removed:
2855 tmp_project_name = '/'.join(project.split('/')[1:])
2856 path = os.path.join(self.upstream_root, tmp_project_name)
2857 repo = git.Repo(path)
2858 repos[project] = repo
2859 return repos
2860
James E. Blair3f876d52016-07-22 13:07:14 -07002861
2862class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002863 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002864 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002865
Jamie Lennox7655b552017-03-17 12:33:38 +11002866 @contextmanager
2867 def jobLog(self, build):
2868 """Print job logs on assertion errors
2869
2870 This method is a context manager which, if it encounters an
2871 ecxeption, adds the build log to the debug output.
2872
2873 :arg Build build: The build that's being asserted.
2874 """
2875 try:
2876 yield
2877 except Exception:
2878 path = os.path.join(self.test_root, build.uuid,
2879 'work', 'logs', 'job-output.txt')
2880 with open(path) as f:
2881 self.log.debug(f.read())
2882 raise
2883
Joshua Heskethd78b4482015-09-14 16:56:34 -06002884
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002885class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002886 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002887 use_ssl = True
2888
2889
Joshua Heskethd78b4482015-09-14 16:56:34 -06002890class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002891 def setup_config(self):
2892 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002893 for section_name in self.config.sections():
2894 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2895 section_name, re.I)
2896 if not con_match:
2897 continue
2898
2899 if self.config.get(section_name, 'driver') == 'sql':
2900 f = MySQLSchemaFixture()
2901 self.useFixture(f)
2902 if (self.config.get(section_name, 'dburi') ==
2903 '$MYSQL_FIXTURE_DBURI$'):
2904 self.config.set(section_name, 'dburi', f.dburi)