blob: 20e664edfe731a527bed87bb7caa82a6fa37cf28 [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': [],
James E. Blair0e4c7912018-01-02 14:20:17 -0800173 'url': 'https://%s/%s' % (self.gerrit.server, number)}
Clark Boylanb640e052014-04-03 16:41:46 -0700174
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. Blair0e4c7912018-01-02 14:20:17 -0800562 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800563 if query.startswith('change:'):
564 # Query a specific changeid
565 changeid = query[len('change:'):]
566 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800567 if (change.data['id'] == changeid or
568 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700569 elif query.startswith('message:'):
570 # Query the content of a commit message
571 msg = query[len('message:'):].strip()
572 l = [change.query() for change in self.changes.values()
573 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800574 else:
575 # Query all open changes
576 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700577 return l
James E. Blairc494d542014-08-06 09:23:52 -0700578
James E. Blair0e4c7912018-01-02 14:20:17 -0800579 def simpleQuery(self, query):
580 self.log.debug("simpleQuery: %s" % query)
581 self.queries.append(query)
582 results = []
583 if query.startswith('(') and 'OR' in query:
584 query = query[1:-2]
585 for q in query.split(' OR '):
586 for r in self._simpleQuery(q):
587 if r not in results:
588 results.append(r)
589 else:
590 results = self._simpleQuery(query)
591 return results
592
Joshua Hesketh352264b2015-08-11 23:42:08 +1000593 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700594 pass
595
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200596 def _uploadPack(self, project):
597 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
598 'multi_ack thin-pack side-band side-band-64k ofs-delta '
599 'shallow no-progress include-tag multi_ack_detailed no-done\n')
600 path = os.path.join(self.upstream_root, project.name)
601 repo = git.Repo(path)
602 for ref in repo.refs:
603 r = ref.object.hexsha + ' ' + ref.path + '\n'
604 ret += '%04x%s' % (len(r) + 4, r)
605 ret += '0000'
606 return ret
607
Joshua Hesketh352264b2015-08-11 23:42:08 +1000608 def getGitUrl(self, project):
James E. Blairda5bb7e2018-01-22 16:12:17 -0800609 return 'file://' + os.path.join(self.upstream_root, project.name)
Joshua Hesketh352264b2015-08-11 23:42:08 +1000610
Clark Boylanb640e052014-04-03 16:41:46 -0700611
Gregory Haynes4fc12542015-04-22 20:38:06 -0700612class GithubChangeReference(git.Reference):
613 _common_path_default = "refs/pull"
614 _points_to_commits_only = True
615
616
617class FakeGithubPullRequest(object):
618
619 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800620 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700621 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700622 """Creates a new PR with several commits.
623 Sends an event about opened PR."""
624 self.github = github
625 self.source = github
626 self.number = number
627 self.project = project
628 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100629 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700630 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100631 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700632 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100633 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700634 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100635 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100636 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800637 self.reviews = []
638 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700639 self.updated_at = None
640 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100641 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100642 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700643 self.state = 'open'
James E. Blair54145e02018-01-10 16:07:41 -0800644 self.url = 'https://%s/%s/pull/%s' % (github.server, project, number)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700645 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100646 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700647 self._updateTimeStamp()
648
Jan Hruban570d01c2016-03-10 21:51:32 +0100649 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700650 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100651 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700652 self._updateTimeStamp()
653
Jan Hruban570d01c2016-03-10 21:51:32 +0100654 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700655 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100656 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700657 self._updateTimeStamp()
658
659 def getPullRequestOpenedEvent(self):
660 return self._getPullRequestEvent('opened')
661
662 def getPullRequestSynchronizeEvent(self):
663 return self._getPullRequestEvent('synchronize')
664
665 def getPullRequestReopenedEvent(self):
666 return self._getPullRequestEvent('reopened')
667
668 def getPullRequestClosedEvent(self):
669 return self._getPullRequestEvent('closed')
670
Jesse Keatinga41566f2017-06-14 18:17:51 -0700671 def getPullRequestEditedEvent(self):
672 return self._getPullRequestEvent('edited')
673
Gregory Haynes4fc12542015-04-22 20:38:06 -0700674 def addComment(self, message):
675 self.comments.append(message)
676 self._updateTimeStamp()
677
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200678 def getCommentAddedEvent(self, text):
679 name = 'issue_comment'
680 data = {
681 'action': 'created',
682 'issue': {
683 'number': self.number
684 },
685 'comment': {
686 'body': text
687 },
688 'repository': {
689 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100690 },
691 'sender': {
692 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200693 }
694 }
695 return (name, data)
696
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800697 def getReviewAddedEvent(self, review):
698 name = 'pull_request_review'
699 data = {
700 'action': 'submitted',
701 'pull_request': {
702 'number': self.number,
703 'title': self.subject,
704 'updated_at': self.updated_at,
705 'base': {
706 'ref': self.branch,
707 'repo': {
708 'full_name': self.project
709 }
710 },
711 'head': {
712 'sha': self.head_sha
713 }
714 },
715 'review': {
716 'state': review
717 },
718 'repository': {
719 'full_name': self.project
720 },
721 'sender': {
722 'login': 'ghuser'
723 }
724 }
725 return (name, data)
726
Jan Hruban16ad31f2015-11-07 14:39:07 +0100727 def addLabel(self, name):
728 if name not in self.labels:
729 self.labels.append(name)
730 self._updateTimeStamp()
731 return self._getLabelEvent(name)
732
733 def removeLabel(self, name):
734 if name in self.labels:
735 self.labels.remove(name)
736 self._updateTimeStamp()
737 return self._getUnlabelEvent(name)
738
739 def _getLabelEvent(self, label):
740 name = 'pull_request'
741 data = {
742 'action': 'labeled',
743 'pull_request': {
744 'number': self.number,
745 'updated_at': self.updated_at,
746 'base': {
747 'ref': self.branch,
748 'repo': {
749 'full_name': self.project
750 }
751 },
752 'head': {
753 'sha': self.head_sha
754 }
755 },
756 'label': {
757 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100758 },
759 'sender': {
760 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100761 }
762 }
763 return (name, data)
764
765 def _getUnlabelEvent(self, label):
766 name = 'pull_request'
767 data = {
768 'action': 'unlabeled',
769 'pull_request': {
770 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100771 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100772 'updated_at': self.updated_at,
773 'base': {
774 'ref': self.branch,
775 'repo': {
776 'full_name': self.project
777 }
778 },
779 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800780 'sha': self.head_sha,
781 'repo': {
782 'full_name': self.project
783 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100784 }
785 },
786 'label': {
787 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100788 },
789 'sender': {
790 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100791 }
792 }
793 return (name, data)
794
Jesse Keatinga41566f2017-06-14 18:17:51 -0700795 def editBody(self, body):
796 self.body = body
797 self._updateTimeStamp()
798
Gregory Haynes4fc12542015-04-22 20:38:06 -0700799 def _getRepo(self):
800 repo_path = os.path.join(self.upstream_root, self.project)
801 return git.Repo(repo_path)
802
803 def _createPRRef(self):
804 repo = self._getRepo()
805 GithubChangeReference.create(
806 repo, self._getPRReference(), 'refs/tags/init')
807
Jan Hruban570d01c2016-03-10 21:51:32 +0100808 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700809 repo = self._getRepo()
810 ref = repo.references[self._getPRReference()]
811 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100812 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700813 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100814 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700815 repo.head.reference = ref
816 zuul.merger.merger.reset_repo_to_head(repo)
817 repo.git.clean('-x', '-f', '-d')
818
Jan Hruban570d01c2016-03-10 21:51:32 +0100819 if files:
820 fn = files[0]
821 self.files = files
822 else:
823 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
824 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100825 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700826 fn = os.path.join(repo.working_dir, fn)
827 f = open(fn, 'w')
828 with open(fn, 'w') as f:
829 f.write("test %s %s\n" %
830 (self.branch, self.number))
831 repo.index.add([fn])
832
833 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800834 # Create an empty set of statuses for the given sha,
835 # each sha on a PR may have a status set on it
836 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700837 repo.head.reference = 'master'
838 zuul.merger.merger.reset_repo_to_head(repo)
839 repo.git.clean('-x', '-f', '-d')
840 repo.heads['master'].checkout()
841
842 def _updateTimeStamp(self):
843 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
844
845 def getPRHeadSha(self):
846 repo = self._getRepo()
847 return repo.references[self._getPRReference()].commit.hexsha
848
Jesse Keatingae4cd272017-01-30 17:10:44 -0800849 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800850 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
851 # convert the timestamp to a str format that would be returned
852 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800853
Adam Gandelmand81dd762017-02-09 15:15:49 -0800854 if granted_on:
855 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
856 submitted_at = time.strftime(
857 gh_time_format, granted_on.timetuple())
858 else:
859 # github timestamps only down to the second, so we need to make
860 # sure reviews that tests add appear to be added over a period of
861 # time in the past and not all at once.
862 if not self.reviews:
863 # the first review happens 10 mins ago
864 offset = 600
865 else:
866 # subsequent reviews happen 1 minute closer to now
867 offset = 600 - (len(self.reviews) * 60)
868
869 granted_on = datetime.datetime.utcfromtimestamp(
870 time.time() - offset)
871 submitted_at = time.strftime(
872 gh_time_format, granted_on.timetuple())
873
Jesse Keatingae4cd272017-01-30 17:10:44 -0800874 self.reviews.append({
875 'state': state,
876 'user': {
877 'login': user,
878 'email': user + "@derp.com",
879 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800880 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800881 })
882
Gregory Haynes4fc12542015-04-22 20:38:06 -0700883 def _getPRReference(self):
884 return '%s/head' % self.number
885
886 def _getPullRequestEvent(self, action):
887 name = 'pull_request'
888 data = {
889 'action': action,
890 'number': self.number,
891 'pull_request': {
892 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100893 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700894 'updated_at': self.updated_at,
895 'base': {
896 'ref': self.branch,
897 'repo': {
898 'full_name': self.project
899 }
900 },
901 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800902 'sha': self.head_sha,
903 'repo': {
904 'full_name': self.project
905 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700906 },
907 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100908 },
909 'sender': {
910 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700911 }
912 }
913 return (name, data)
914
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800915 def getCommitStatusEvent(self, context, state='success', user='zuul'):
916 name = 'status'
917 data = {
918 'state': state,
919 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700920 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800921 'description': 'Test results for %s: %s' % (self.head_sha, state),
922 'target_url': 'http://zuul/%s' % self.head_sha,
923 'branches': [],
924 'context': context,
925 'sender': {
926 'login': user
927 }
928 }
929 return (name, data)
930
James E. Blair289f5932017-07-27 15:02:29 -0700931 def setMerged(self, commit_message):
932 self.is_merged = True
933 self.merge_message = commit_message
934
935 repo = self._getRepo()
936 repo.heads[self.branch].commit = repo.commit(self.head_sha)
937
Gregory Haynes4fc12542015-04-22 20:38:06 -0700938
939class FakeGithubConnection(githubconnection.GithubConnection):
940 log = logging.getLogger("zuul.test.FakeGithubConnection")
941
942 def __init__(self, driver, connection_name, connection_config,
James E. Blair6bacffb2018-01-05 13:45:25 -0800943 changes_db=None, upstream_root=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700944 super(FakeGithubConnection, self).__init__(driver, connection_name,
945 connection_config)
946 self.connection_name = connection_name
947 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800948 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700949 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700950 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100951 self.merge_failure = False
952 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100953 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800954 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Tobias Henkel64e37a02017-08-02 10:13:30 +0200955
956 def getGithubClient(self,
957 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600958 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200959 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700960
Jesse Keatinga41566f2017-06-14 18:17:51 -0700961 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700962 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700963 self.pr_number += 1
964 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100965 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700966 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800967 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 return pull_request
969
Jesse Keating71a47ff2017-06-06 11:36:43 -0700970 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
971 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700972 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700973 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700974 if not new_rev:
975 new_rev = random_sha1()
976 name = 'push'
977 data = {
978 'ref': ref,
979 'before': old_rev,
980 'after': new_rev,
981 'repository': {
982 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700983 },
984 'commits': [
985 {
986 'added': added_files,
987 'removed': removed_files,
988 'modified': modified_files
989 }
990 ]
Wayne1a78c612015-06-11 17:14:13 -0700991 }
992 return (name, data)
993
Gregory Haynes4fc12542015-04-22 20:38:06 -0700994 def emitEvent(self, event):
995 """Emulates sending the GitHub webhook event to the connection."""
996 port = self.webapp.server.socket.getsockname()[1]
997 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700998 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -0700999 secret = self.connection_config['webhook_token']
1000 signature = githubconnection._sign_request(payload, secret)
1001 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001002 req = urllib.request.Request(
1003 'http://localhost:%s/connection/%s/payload'
1004 % (port, self.connection_name),
1005 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001006 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001007
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001008 def addProject(self, project):
1009 # use the original method here and additionally register it in the
1010 # fake github
1011 super(FakeGithubConnection, self).addProject(project)
1012 self.getGithubClient(project).addProject(project)
1013
Jesse Keating9021a012017-08-29 14:45:27 -07001014 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001015 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001016 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001017 if len(prs) > 1:
1018 raise Exception('Multiple pulls found with head sha: %s' % sha)
1019 pr = prs[0]
1020 return self.getPull(pr.project, pr.number)
1021
Jesse Keatingae4cd272017-01-30 17:10:44 -08001022 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001023 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001024 return pr.reviews
1025
Jesse Keatingae4cd272017-01-30 17:10:44 -08001026 def getRepoPermission(self, project, login):
1027 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001028 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001029 pr_owner, pr_project = pr.project.split('/')
1030 if (pr_owner == owner and proj == pr_project):
1031 if login in pr.writers:
1032 return 'write'
1033 else:
1034 return 'read'
1035
Gregory Haynes4fc12542015-04-22 20:38:06 -07001036 def getGitUrl(self, project):
1037 return os.path.join(self.upstream_root, str(project))
1038
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001039 def real_getGitUrl(self, project):
1040 return super(FakeGithubConnection, self).getGitUrl(project)
1041
Jan Hrubane252a732017-01-03 15:03:09 +01001042 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001043 # record that this got reported
1044 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001045 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001046 pull_request.addComment(message)
1047
Jan Hruban3b415922016-02-03 13:10:22 +01001048 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001049 # record that this got reported
1050 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001051 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001052 if self.merge_failure:
1053 raise Exception('Pull request was not merged')
1054 if self.merge_not_allowed_count > 0:
1055 self.merge_not_allowed_count -= 1
1056 raise MergeFailure('Merge was not successful due to mergeability'
1057 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001058 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001059
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001060 def setCommitStatus(self, project, sha, state, url='', description='',
1061 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001062 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001063 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001064 super(FakeGithubConnection, self).setCommitStatus(
1065 project, sha, state,
1066 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001067
Jan Hruban16ad31f2015-11-07 14:39:07 +01001068 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001069 # record that this got reported
1070 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001071 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001072 pull_request.addLabel(label)
1073
1074 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001075 # record that this got reported
1076 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001077 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001078 pull_request.removeLabel(label)
1079
Gregory Haynes4fc12542015-04-22 20:38:06 -07001080
Clark Boylanb640e052014-04-03 16:41:46 -07001081class BuildHistory(object):
1082 def __init__(self, **kw):
1083 self.__dict__.update(kw)
1084
1085 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001086 return ("<Completed build, result: %s name: %s uuid: %s "
1087 "changes: %s ref: %s>" %
1088 (self.result, self.name, self.uuid,
1089 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001090
1091
Clark Boylanb640e052014-04-03 16:41:46 -07001092class FakeStatsd(threading.Thread):
1093 def __init__(self):
1094 threading.Thread.__init__(self)
1095 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001096 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001097 self.sock.bind(('', 0))
1098 self.port = self.sock.getsockname()[1]
1099 self.wake_read, self.wake_write = os.pipe()
1100 self.stats = []
1101
1102 def run(self):
1103 while True:
1104 poll = select.poll()
1105 poll.register(self.sock, select.POLLIN)
1106 poll.register(self.wake_read, select.POLLIN)
1107 ret = poll.poll()
1108 for (fd, event) in ret:
1109 if fd == self.sock.fileno():
1110 data = self.sock.recvfrom(1024)
1111 if not data:
1112 return
1113 self.stats.append(data[0])
1114 if fd == self.wake_read:
1115 return
1116
1117 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001118 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001119
1120
James E. Blaire1767bc2016-08-02 10:00:27 -07001121class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001122 log = logging.getLogger("zuul.test")
1123
Paul Belanger174a8272017-03-14 13:20:10 -04001124 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001125 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001126 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001127 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001128 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001129 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001130 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001131 # TODOv3(jeblair): self.node is really "the label of the node
1132 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001133 # keep using it like this, or we may end up exposing more of
1134 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001135 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001136 self.node = None
1137 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001138 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001139 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001140 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001141 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001142 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001143 self.wait_condition = threading.Condition()
1144 self.waiting = False
1145 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001146 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001147 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001148 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001149 items = self.parameters['zuul']['items']
1150 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1151 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001152
James E. Blair3158e282016-08-19 09:34:11 -07001153 def __repr__(self):
1154 waiting = ''
1155 if self.waiting:
1156 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001157 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1158 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001159
Clark Boylanb640e052014-04-03 16:41:46 -07001160 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001161 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001162 self.wait_condition.acquire()
1163 self.wait_condition.notify()
1164 self.waiting = False
1165 self.log.debug("Build %s released" % self.unique)
1166 self.wait_condition.release()
1167
1168 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001169 """Return whether this build is being held.
1170
1171 :returns: Whether the build is being held.
1172 :rtype: bool
1173 """
1174
Clark Boylanb640e052014-04-03 16:41:46 -07001175 self.wait_condition.acquire()
1176 if self.waiting:
1177 ret = True
1178 else:
1179 ret = False
1180 self.wait_condition.release()
1181 return ret
1182
1183 def _wait(self):
1184 self.wait_condition.acquire()
1185 self.waiting = True
1186 self.log.debug("Build %s waiting" % self.unique)
1187 self.wait_condition.wait()
1188 self.wait_condition.release()
1189
1190 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001191 self.log.debug('Running build %s' % self.unique)
1192
Paul Belanger174a8272017-03-14 13:20:10 -04001193 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001194 self.log.debug('Holding build %s' % self.unique)
1195 self._wait()
1196 self.log.debug("Build %s continuing" % self.unique)
1197
James E. Blair412fba82017-01-26 15:00:50 -08001198 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001199 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001200 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001201 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001202 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001203 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001204 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001205
James E. Blaire1767bc2016-08-02 10:00:27 -07001206 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001207
James E. Blaira5dba232016-08-08 15:53:24 -07001208 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001209 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001210 for change in changes:
1211 if self.hasChanges(change):
1212 return True
1213 return False
1214
James E. Blaire7b99a02016-08-05 14:27:34 -07001215 def hasChanges(self, *changes):
1216 """Return whether this build has certain changes in its git repos.
1217
1218 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001219 are expected to be present (in order) in the git repository of
1220 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001221
1222 :returns: Whether the build has the indicated changes.
1223 :rtype: bool
1224
1225 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001226 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001227 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001228 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001229 try:
1230 repo = git.Repo(path)
1231 except NoSuchPathError as e:
1232 self.log.debug('%s' % e)
1233 return False
James E. Blair247cab72017-07-20 16:52:36 -07001234 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001235 commit_message = '%s-1' % change.subject
1236 self.log.debug("Checking if build %s has changes; commit_message "
1237 "%s; repo_messages %s" % (self, commit_message,
1238 repo_messages))
1239 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001240 self.log.debug(" messages do not match")
1241 return False
1242 self.log.debug(" OK")
1243 return True
1244
James E. Blaird8af5422017-05-24 13:59:40 -07001245 def getWorkspaceRepos(self, projects):
1246 """Return workspace git repo objects for the listed projects
1247
1248 :arg list projects: A list of strings, each the canonical name
1249 of a project.
1250
1251 :returns: A dictionary of {name: repo} for every listed
1252 project.
1253 :rtype: dict
1254
1255 """
1256
1257 repos = {}
1258 for project in projects:
1259 path = os.path.join(self.jobdir.src_root, project)
1260 repo = git.Repo(path)
1261 repos[project] = repo
1262 return repos
1263
Clark Boylanb640e052014-04-03 16:41:46 -07001264
James E. Blair107bb252017-10-13 15:53:16 -07001265class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1266 def doMergeChanges(self, merger, items, repo_state):
1267 # Get a merger in order to update the repos involved in this job.
1268 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1269 merger, items, repo_state)
1270 if not commit: # merge conflict
1271 self.recordResult('MERGER_FAILURE')
1272 return commit
1273
1274 def recordResult(self, result):
1275 build = self.executor_server.job_builds[self.job.unique]
1276 self.executor_server.lock.acquire()
1277 self.executor_server.build_history.append(
1278 BuildHistory(name=build.name, result=result, changes=build.changes,
1279 node=build.node, uuid=build.unique,
1280 ref=build.parameters['zuul']['ref'],
1281 parameters=build.parameters, jobdir=build.jobdir,
1282 pipeline=build.parameters['zuul']['pipeline'])
1283 )
1284 self.executor_server.running_builds.remove(build)
1285 del self.executor_server.job_builds[self.job.unique]
1286 self.executor_server.lock.release()
1287
1288 def runPlaybooks(self, args):
1289 build = self.executor_server.job_builds[self.job.unique]
1290 build.jobdir = self.jobdir
1291
1292 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1293 self.recordResult(result)
1294 return result
1295
James E. Blaira86aaf12017-10-15 20:59:50 -07001296 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001297 build = self.executor_server.job_builds[self.job.unique]
1298
1299 if self.executor_server._run_ansible:
1300 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001301 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001302 else:
1303 if playbook.path:
1304 result = build.run()
1305 else:
1306 result = (self.RESULT_NORMAL, 0)
1307 return result
1308
1309 def getHostList(self, args):
1310 self.log.debug("hostlist")
1311 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1312 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001313 if not host['host_vars'].get('ansible_connection'):
1314 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001315
1316 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001317 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001318 host_vars=dict(ansible_connection='local'),
1319 host_keys=[]))
1320 return hosts
1321
1322
Paul Belanger174a8272017-03-14 13:20:10 -04001323class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1324 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001325
Paul Belanger174a8272017-03-14 13:20:10 -04001326 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001327 they will report that they have started but then pause until
1328 released before reporting completion. This attribute may be
1329 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001330 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001331 be explicitly released.
1332
1333 """
James E. Blairfaf81982017-10-10 15:42:26 -07001334
1335 _job_class = RecordingAnsibleJob
1336
James E. Blairf5dbd002015-12-23 15:26:17 -08001337 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001338 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001339 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001340 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001341 self.hold_jobs_in_build = False
1342 self.lock = threading.Lock()
1343 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001344 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001345 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001346 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001347
James E. Blaira5dba232016-08-08 15:53:24 -07001348 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001349 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001350
1351 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001352 :arg Change change: The :py:class:`~tests.base.FakeChange`
1353 instance which should cause the job to fail. This job
1354 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001355
1356 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001357 l = self.fail_tests.get(name, [])
1358 l.append(change)
1359 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001360
James E. Blair962220f2016-08-03 11:22:38 -07001361 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001362 """Release a held build.
1363
1364 :arg str regex: A regular expression which, if supplied, will
1365 cause only builds with matching names to be released. If
1366 not supplied, all builds will be released.
1367
1368 """
James E. Blair962220f2016-08-03 11:22:38 -07001369 builds = self.running_builds[:]
1370 self.log.debug("Releasing build %s (%s)" % (regex,
1371 len(self.running_builds)))
1372 for build in builds:
1373 if not regex or re.match(regex, build.name):
1374 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001375 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001376 build.release()
1377 else:
1378 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001379 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001380 self.log.debug("Done releasing builds %s (%s)" %
1381 (regex, len(self.running_builds)))
1382
Paul Belanger174a8272017-03-14 13:20:10 -04001383 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001384 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001385 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001386 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001387 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001388 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001389 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001390 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001391 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001392
1393 def stopJob(self, job):
1394 self.log.debug("handle stop")
1395 parameters = json.loads(job.arguments)
1396 uuid = parameters['uuid']
1397 for build in self.running_builds:
1398 if build.unique == uuid:
1399 build.aborted = True
1400 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001401 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001402
James E. Blaira002b032017-04-18 10:35:48 -07001403 def stop(self):
1404 for build in self.running_builds:
1405 build.release()
1406 super(RecordingExecutorServer, self).stop()
1407
Joshua Hesketh50c21782016-10-13 21:34:14 +11001408
Clark Boylanb640e052014-04-03 16:41:46 -07001409class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001410 """A Gearman server for use in tests.
1411
1412 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1413 added to the queue but will not be distributed to workers
1414 until released. This attribute may be changed at any time and
1415 will take effect for subsequently enqueued jobs, but
1416 previously held jobs will still need to be explicitly
1417 released.
1418
1419 """
1420
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001421 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001422 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001423 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001424 if use_ssl:
1425 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1426 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1427 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1428 else:
1429 ssl_ca = None
1430 ssl_cert = None
1431 ssl_key = None
1432
1433 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1434 ssl_cert=ssl_cert,
1435 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001436
1437 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001438 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1439 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001440 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001441 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001442 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001443 elif job.name.startswith(b'merger:'):
1444 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001445 else:
1446 job.waiting = False
1447 if job.waiting:
1448 continue
1449 if job.name in connection.functions:
1450 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001451 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001452 connection.related_jobs[job.handle] = job
1453 job.worker_connection = connection
1454 job.running = True
1455 return job
1456 return None
1457
1458 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001459 """Release a held job.
1460
1461 :arg str regex: A regular expression which, if supplied, will
1462 cause only jobs with matching names to be released. If
1463 not supplied, all jobs will be released.
1464 """
Clark Boylanb640e052014-04-03 16:41:46 -07001465 released = False
1466 qlen = (len(self.high_queue) + len(self.normal_queue) +
1467 len(self.low_queue))
1468 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1469 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001470 match = False
1471 if job.name == b'executor:execute':
1472 parameters = json.loads(job.arguments.decode('utf8'))
1473 if not regex or re.match(regex, parameters.get('job')):
1474 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001475 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001476 if not regex:
1477 match = True
1478 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001479 self.log.debug("releasing queued job %s" %
1480 job.unique)
1481 job.waiting = False
1482 released = True
1483 else:
1484 self.log.debug("not releasing queued job %s" %
1485 job.unique)
1486 if released:
1487 self.wakeConnections()
1488 qlen = (len(self.high_queue) + len(self.normal_queue) +
1489 len(self.low_queue))
1490 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1491
1492
1493class FakeSMTP(object):
1494 log = logging.getLogger('zuul.FakeSMTP')
1495
1496 def __init__(self, messages, server, port):
1497 self.server = server
1498 self.port = port
1499 self.messages = messages
1500
1501 def sendmail(self, from_email, to_email, msg):
1502 self.log.info("Sending email from %s, to %s, with msg %s" % (
1503 from_email, to_email, msg))
1504
1505 headers = msg.split('\n\n', 1)[0]
1506 body = msg.split('\n\n', 1)[1]
1507
1508 self.messages.append(dict(
1509 from_email=from_email,
1510 to_email=to_email,
1511 msg=msg,
1512 headers=headers,
1513 body=body,
1514 ))
1515
1516 return True
1517
1518 def quit(self):
1519 return True
1520
1521
James E. Blairdce6cea2016-12-20 16:45:32 -08001522class FakeNodepool(object):
1523 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001524 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001525
1526 log = logging.getLogger("zuul.test.FakeNodepool")
1527
1528 def __init__(self, host, port, chroot):
1529 self.client = kazoo.client.KazooClient(
1530 hosts='%s:%s%s' % (host, port, chroot))
1531 self.client.start()
1532 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001533 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001534 self.thread = threading.Thread(target=self.run)
1535 self.thread.daemon = True
1536 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001537 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001538
1539 def stop(self):
1540 self._running = False
1541 self.thread.join()
1542 self.client.stop()
1543 self.client.close()
1544
1545 def run(self):
1546 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001547 try:
1548 self._run()
1549 except Exception:
1550 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001551 time.sleep(0.1)
1552
1553 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001554 if self.paused:
1555 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001556 for req in self.getNodeRequests():
1557 self.fulfillRequest(req)
1558
1559 def getNodeRequests(self):
1560 try:
1561 reqids = self.client.get_children(self.REQUEST_ROOT)
1562 except kazoo.exceptions.NoNodeError:
1563 return []
1564 reqs = []
1565 for oid in sorted(reqids):
1566 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001567 try:
1568 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001569 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001570 data['_oid'] = oid
1571 reqs.append(data)
1572 except kazoo.exceptions.NoNodeError:
1573 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001574 return reqs
1575
James E. Blaire18d4602017-01-05 11:17:28 -08001576 def getNodes(self):
1577 try:
1578 nodeids = self.client.get_children(self.NODE_ROOT)
1579 except kazoo.exceptions.NoNodeError:
1580 return []
1581 nodes = []
1582 for oid in sorted(nodeids):
1583 path = self.NODE_ROOT + '/' + oid
1584 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001585 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001586 data['_oid'] = oid
1587 try:
1588 lockfiles = self.client.get_children(path + '/lock')
1589 except kazoo.exceptions.NoNodeError:
1590 lockfiles = []
1591 if lockfiles:
1592 data['_lock'] = True
1593 else:
1594 data['_lock'] = False
1595 nodes.append(data)
1596 return nodes
1597
James E. Blaira38c28e2017-01-04 10:33:20 -08001598 def makeNode(self, request_id, node_type):
1599 now = time.time()
1600 path = '/nodepool/nodes/'
1601 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001602 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001603 provider='test-provider',
1604 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001605 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001606 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001607 public_ipv4='127.0.0.1',
1608 private_ipv4=None,
1609 public_ipv6=None,
1610 allocated_to=request_id,
1611 state='ready',
1612 state_time=now,
1613 created_time=now,
1614 updated_time=now,
1615 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001616 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001617 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001618 if 'fakeuser' in node_type:
1619 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001620 if 'windows' in node_type:
1621 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001622 if 'network' in node_type:
1623 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001624
Clint Byrumf322fe22017-05-10 20:53:12 -07001625 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001626 path = self.client.create(path, data,
1627 makepath=True,
1628 sequence=True)
1629 nodeid = path.split("/")[-1]
1630 return nodeid
1631
Krzysztof Klimonda37d54032017-10-25 12:16:47 +02001632 def removeNode(self, node):
1633 path = self.NODE_ROOT + '/' + node["_oid"]
1634 self.client.delete(path, recursive=True)
1635
James E. Blair6ab79e02017-01-06 10:10:17 -08001636 def addFailRequest(self, request):
1637 self.fail_requests.add(request['_oid'])
1638
James E. Blairdce6cea2016-12-20 16:45:32 -08001639 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001640 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001641 return
1642 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001643 oid = request['_oid']
1644 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001645
James E. Blair6ab79e02017-01-06 10:10:17 -08001646 if oid in self.fail_requests:
1647 request['state'] = 'failed'
1648 else:
1649 request['state'] = 'fulfilled'
1650 nodes = []
1651 for node in request['node_types']:
1652 nodeid = self.makeNode(oid, node)
1653 nodes.append(nodeid)
1654 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001655
James E. Blaira38c28e2017-01-04 10:33:20 -08001656 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001657 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001658 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001659 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001660 try:
1661 self.client.set(path, data)
1662 except kazoo.exceptions.NoNodeError:
1663 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001664
1665
James E. Blair498059b2016-12-20 13:50:13 -08001666class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001667 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001668 super(ChrootedKazooFixture, self).__init__()
1669
1670 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1671 if ':' in zk_host:
1672 host, port = zk_host.split(':')
1673 else:
1674 host = zk_host
1675 port = None
1676
1677 self.zookeeper_host = host
1678
1679 if not port:
1680 self.zookeeper_port = 2181
1681 else:
1682 self.zookeeper_port = int(port)
1683
Clark Boylan621ec9a2017-04-07 17:41:33 -07001684 self.test_id = test_id
1685
James E. Blair498059b2016-12-20 13:50:13 -08001686 def _setUp(self):
1687 # Make sure the test chroot paths do not conflict
1688 random_bits = ''.join(random.choice(string.ascii_lowercase +
1689 string.ascii_uppercase)
1690 for x in range(8))
1691
Clark Boylan621ec9a2017-04-07 17:41:33 -07001692 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001693 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1694
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001695 self.addCleanup(self._cleanup)
1696
James E. Blair498059b2016-12-20 13:50:13 -08001697 # Ensure the chroot path exists and clean up any pre-existing znodes.
1698 _tmp_client = kazoo.client.KazooClient(
1699 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1700 _tmp_client.start()
1701
1702 if _tmp_client.exists(self.zookeeper_chroot):
1703 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1704
1705 _tmp_client.ensure_path(self.zookeeper_chroot)
1706 _tmp_client.stop()
1707 _tmp_client.close()
1708
James E. Blair498059b2016-12-20 13:50:13 -08001709 def _cleanup(self):
1710 '''Remove the chroot path.'''
1711 # Need a non-chroot'ed client to remove the chroot path
1712 _tmp_client = kazoo.client.KazooClient(
1713 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1714 _tmp_client.start()
1715 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1716 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001717 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001718
1719
Joshua Heskethd78b4482015-09-14 16:56:34 -06001720class MySQLSchemaFixture(fixtures.Fixture):
1721 def setUp(self):
1722 super(MySQLSchemaFixture, self).setUp()
1723
1724 random_bits = ''.join(random.choice(string.ascii_lowercase +
1725 string.ascii_uppercase)
1726 for x in range(8))
1727 self.name = '%s_%s' % (random_bits, os.getpid())
1728 self.passwd = uuid.uuid4().hex
1729 db = pymysql.connect(host="localhost",
1730 user="openstack_citest",
1731 passwd="openstack_citest",
1732 db="openstack_citest")
1733 cur = db.cursor()
1734 cur.execute("create database %s" % self.name)
1735 cur.execute(
1736 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1737 (self.name, self.name, self.passwd))
1738 cur.execute("flush privileges")
1739
1740 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1741 self.passwd,
1742 self.name)
1743 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1744 self.addCleanup(self.cleanup)
1745
1746 def cleanup(self):
1747 db = pymysql.connect(host="localhost",
1748 user="openstack_citest",
1749 passwd="openstack_citest",
1750 db="openstack_citest")
1751 cur = db.cursor()
1752 cur.execute("drop database %s" % self.name)
1753 cur.execute("drop user '%s'@'localhost'" % self.name)
1754 cur.execute("flush privileges")
1755
1756
Maru Newby3fe5f852015-01-13 04:22:14 +00001757class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001758 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001759 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001760
James E. Blair1c236df2017-02-01 14:07:24 -08001761 def attachLogs(self, *args):
1762 def reader():
1763 self._log_stream.seek(0)
1764 while True:
1765 x = self._log_stream.read(4096)
1766 if not x:
1767 break
1768 yield x.encode('utf8')
1769 content = testtools.content.content_from_reader(
1770 reader,
1771 testtools.content_type.UTF8_TEXT,
1772 False)
1773 self.addDetail('logging', content)
1774
Clark Boylanb640e052014-04-03 16:41:46 -07001775 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001776 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001777 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1778 try:
1779 test_timeout = int(test_timeout)
1780 except ValueError:
1781 # If timeout value is invalid do not set a timeout.
1782 test_timeout = 0
1783 if test_timeout > 0:
1784 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1785
1786 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1787 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1788 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1789 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1790 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1791 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1792 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1793 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1794 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1795 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001796 self._log_stream = StringIO()
1797 self.addOnException(self.attachLogs)
1798 else:
1799 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001800
James E. Blair1c236df2017-02-01 14:07:24 -08001801 handler = logging.StreamHandler(self._log_stream)
1802 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1803 '%(levelname)-8s %(message)s')
1804 handler.setFormatter(formatter)
1805
1806 logger = logging.getLogger()
1807 logger.setLevel(logging.DEBUG)
1808 logger.addHandler(handler)
1809
Clark Boylan3410d532017-04-25 12:35:29 -07001810 # Make sure we don't carry old handlers around in process state
1811 # which slows down test runs
1812 self.addCleanup(logger.removeHandler, handler)
1813 self.addCleanup(handler.close)
1814 self.addCleanup(handler.flush)
1815
James E. Blair1c236df2017-02-01 14:07:24 -08001816 # NOTE(notmorgan): Extract logging overrides for specific
1817 # libraries from the OS_LOG_DEFAULTS env and create loggers
1818 # for each. This is used to limit the output during test runs
1819 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001820 log_defaults_from_env = os.environ.get(
1821 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001822 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001823
James E. Blairdce6cea2016-12-20 16:45:32 -08001824 if log_defaults_from_env:
1825 for default in log_defaults_from_env.split(','):
1826 try:
1827 name, level_str = default.split('=', 1)
1828 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001829 logger = logging.getLogger(name)
1830 logger.setLevel(level)
1831 logger.addHandler(handler)
1832 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001833 except ValueError:
1834 # NOTE(notmorgan): Invalid format of the log default,
1835 # skip and don't try and apply a logger for the
1836 # specified module
1837 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001838
Maru Newby3fe5f852015-01-13 04:22:14 +00001839
1840class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001841 """A test case with a functioning Zuul.
1842
1843 The following class variables are used during test setup and can
1844 be overidden by subclasses but are effectively read-only once a
1845 test method starts running:
1846
1847 :cvar str config_file: This points to the main zuul config file
1848 within the fixtures directory. Subclasses may override this
1849 to obtain a different behavior.
1850
1851 :cvar str tenant_config_file: This is the tenant config file
1852 (which specifies from what git repos the configuration should
1853 be loaded). It defaults to the value specified in
1854 `config_file` but can be overidden by subclasses to obtain a
1855 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001856 configuration. See also the :py:func:`simple_layout`
1857 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001858
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001859 :cvar bool create_project_keys: Indicates whether Zuul should
1860 auto-generate keys for each project, or whether the test
1861 infrastructure should insert dummy keys to save time during
1862 startup. Defaults to False.
1863
James E. Blaire7b99a02016-08-05 14:27:34 -07001864 The following are instance variables that are useful within test
1865 methods:
1866
1867 :ivar FakeGerritConnection fake_<connection>:
1868 A :py:class:`~tests.base.FakeGerritConnection` will be
1869 instantiated for each connection present in the config file
1870 and stored here. For instance, `fake_gerrit` will hold the
1871 FakeGerritConnection object for a connection named `gerrit`.
1872
1873 :ivar FakeGearmanServer gearman_server: An instance of
1874 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1875 server that all of the Zuul components in this test use to
1876 communicate with each other.
1877
Paul Belanger174a8272017-03-14 13:20:10 -04001878 :ivar RecordingExecutorServer executor_server: An instance of
1879 :py:class:`~tests.base.RecordingExecutorServer` which is the
1880 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001881
1882 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1883 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001884 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001885 list upon completion.
1886
1887 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1888 objects representing completed builds. They are appended to
1889 the list in the order they complete.
1890
1891 """
1892
James E. Blair83005782015-12-11 14:46:03 -08001893 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001894 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001895 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001896 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001897
1898 def _startMerger(self):
1899 self.merge_server = zuul.merger.server.MergeServer(self.config,
1900 self.connections)
1901 self.merge_server.start()
1902
Maru Newby3fe5f852015-01-13 04:22:14 +00001903 def setUp(self):
1904 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001905
1906 self.setupZK()
1907
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001908 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001909 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001910 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1911 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001912 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001913 tmp_root = tempfile.mkdtemp(
1914 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001915 self.test_root = os.path.join(tmp_root, "zuul-test")
1916 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001917 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001918 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001919 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001920 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1921 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001922
1923 if os.path.exists(self.test_root):
1924 shutil.rmtree(self.test_root)
1925 os.makedirs(self.test_root)
1926 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001927 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001928 os.makedirs(self.merger_state_root)
1929 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001930
1931 # Make per test copy of Configuration.
1932 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001933 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1934 if not os.path.exists(self.private_key_file):
1935 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1936 shutil.copy(src_private_key_file, self.private_key_file)
1937 shutil.copy('{}.pub'.format(src_private_key_file),
1938 '{}.pub'.format(self.private_key_file))
1939 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001940 self.config.set('scheduler', 'tenant_config',
1941 os.path.join(
1942 FIXTURE_DIR,
1943 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001944 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001945 self.config.set(
1946 'scheduler', 'command_socket',
1947 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001948 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001949 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001950 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001951 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001952 self.config.set(
1953 'executor', 'command_socket',
1954 os.path.join(self.test_root, 'executor.socket'))
James E. Blairda5bb7e2018-01-22 16:12:17 -08001955 self.config.set(
1956 'merger', 'command_socket',
1957 os.path.join(self.test_root, 'merger.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001958
Clark Boylanb640e052014-04-03 16:41:46 -07001959 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001960 if self.config.has_section('statsd'):
1961 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001962 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001963
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001964 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001965
1966 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001967 self.log.info("Gearman server on port %s" %
1968 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001969 if self.use_ssl:
1970 self.log.info('SSL enabled for gearman')
1971 self.config.set(
1972 'gearman', 'ssl_ca',
1973 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1974 self.config.set(
1975 'gearman', 'ssl_cert',
1976 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1977 self.config.set(
1978 'gearman', 'ssl_key',
1979 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001980
James E. Blaire511d2f2016-12-08 15:22:26 -08001981 gerritsource.GerritSource.replication_timeout = 1.5
1982 gerritsource.GerritSource.replication_retry_interval = 0.5
1983 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001984
Joshua Hesketh352264b2015-08-11 23:42:08 +10001985 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07001986 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07001987
Jan Hruban7083edd2015-08-21 14:00:54 +02001988 self.webapp = zuul.webapp.WebApp(
1989 self.sched, port=0, listen_address='127.0.0.1')
1990
Jan Hruban6b71aff2015-10-22 16:58:08 +02001991 self.event_queues = [
1992 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001993 self.sched.trigger_event_queue,
1994 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001995 ]
1996
James E. Blairfef78942016-03-11 16:28:56 -08001997 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001998 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001999
Paul Belanger174a8272017-03-14 13:20:10 -04002000 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002001 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002002 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002003 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002004 _test_root=self.test_root,
2005 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002006 self.executor_server.start()
2007 self.history = self.executor_server.build_history
2008 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002009
Paul Belanger174a8272017-03-14 13:20:10 -04002010 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002011 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002012 self.merge_client = zuul.merger.client.MergeClient(
2013 self.config, self.sched)
James E. Blairda5bb7e2018-01-22 16:12:17 -08002014 self.merge_server = None
James E. Blair8d692392016-04-08 17:47:58 -07002015 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002016 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002017 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002018
James E. Blair0d5a36e2017-02-21 10:53:44 -05002019 self.fake_nodepool = FakeNodepool(
2020 self.zk_chroot_fixture.zookeeper_host,
2021 self.zk_chroot_fixture.zookeeper_port,
2022 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002023
Paul Belanger174a8272017-03-14 13:20:10 -04002024 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002025 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002026 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002027 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002028
Clark Boylanb640e052014-04-03 16:41:46 -07002029 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002030 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002031 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002032 # Cleanups are run in reverse order
2033 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002034 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002035 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002036
James E. Blairb9c0d772017-03-03 14:34:49 -08002037 self.sched.reconfigure(self.config)
2038 self.sched.resume()
2039
Tobias Henkel7df274b2017-05-26 17:41:11 +02002040 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002041 # Set up gerrit related fakes
2042 # Set a changes database so multiple FakeGerrit's can report back to
2043 # a virtual canonical database given by the configured hostname
2044 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002045 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002046
2047 def getGerritConnection(driver, name, config):
2048 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2049 con = FakeGerritConnection(driver, name, config,
2050 changes_db=db,
2051 upstream_root=self.upstream_root)
2052 self.event_queues.append(con.event_queue)
2053 setattr(self, 'fake_' + name, con)
2054 return con
2055
2056 self.useFixture(fixtures.MonkeyPatch(
2057 'zuul.driver.gerrit.GerritDriver.getConnection',
2058 getGerritConnection))
2059
Gregory Haynes4fc12542015-04-22 20:38:06 -07002060 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002061 server = config.get('server', 'github.com')
2062 db = self.github_changes_dbs.setdefault(server, {})
Gregory Haynes4fc12542015-04-22 20:38:06 -07002063 con = FakeGithubConnection(driver, name, config,
James E. Blair6bacffb2018-01-05 13:45:25 -08002064 changes_db=db,
Gregory Haynes4fc12542015-04-22 20:38:06 -07002065 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002066 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002067 setattr(self, 'fake_' + name, con)
2068 return con
2069
2070 self.useFixture(fixtures.MonkeyPatch(
2071 'zuul.driver.github.GithubDriver.getConnection',
2072 getGithubConnection))
2073
James E. Blaire511d2f2016-12-08 15:22:26 -08002074 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002075 # TODO(jhesketh): This should come from lib.connections for better
2076 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002077 # Register connections from the config
2078 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002079
Joshua Hesketh352264b2015-08-11 23:42:08 +10002080 def FakeSMTPFactory(*args, **kw):
2081 args = [self.smtp_messages] + list(args)
2082 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002083
Joshua Hesketh352264b2015-08-11 23:42:08 +10002084 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002085
James E. Blaire511d2f2016-12-08 15:22:26 -08002086 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002087 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002088 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002089
James E. Blair83005782015-12-11 14:46:03 -08002090 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002091 # This creates the per-test configuration object. It can be
2092 # overriden by subclasses, but should not need to be since it
2093 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002094 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002095 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002096
James E. Blair39840362017-06-23 20:34:02 +01002097 sections = ['zuul', 'scheduler', 'executor', 'merger']
2098 for section in sections:
2099 if not self.config.has_section(section):
2100 self.config.add_section(section)
2101
James E. Blair06cc3922017-04-19 10:08:10 -07002102 if not self.setupSimpleLayout():
2103 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002104 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002105 self.tenant_config_file)
2106 git_path = os.path.join(
2107 os.path.dirname(
2108 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2109 'git')
2110 if os.path.exists(git_path):
2111 for reponame in os.listdir(git_path):
2112 project = reponame.replace('_', '/')
2113 self.copyDirToRepo(project,
2114 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002115 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002116 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002117 self.setupAllProjectKeys()
2118
James E. Blair06cc3922017-04-19 10:08:10 -07002119 def setupSimpleLayout(self):
2120 # If the test method has been decorated with a simple_layout,
2121 # use that instead of the class tenant_config_file. Set up a
2122 # single config-project with the specified layout, and
2123 # initialize repos for all of the 'project' entries which
2124 # appear in the layout.
2125 test_name = self.id().split('.')[-1]
2126 test = getattr(self, test_name)
2127 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002128 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002129 else:
2130 return False
2131
James E. Blairb70e55a2017-04-19 12:57:02 -07002132 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002133 path = os.path.join(FIXTURE_DIR, path)
2134 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002135 data = f.read()
2136 layout = yaml.safe_load(data)
2137 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002138 untrusted_projects = []
2139 for item in layout:
2140 if 'project' in item:
2141 name = item['project']['name']
2142 untrusted_projects.append(name)
2143 self.init_repo(name)
2144 self.addCommitToRepo(name, 'initial commit',
2145 files={'README': ''},
2146 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002147 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002148 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002149 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002150 for fn in zuul.configloader.as_list(
2151 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002152 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002153 for fn in zuul.configloader.as_list(
2154 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002155 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002156
2157 root = os.path.join(self.test_root, "config")
2158 if not os.path.exists(root):
2159 os.makedirs(root)
2160 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2161 config = [{'tenant':
2162 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002163 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002164 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002165 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002166 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002167 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002168 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002169 os.path.join(FIXTURE_DIR, f.name))
2170
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002171 self.init_repo('org/common-config')
2172 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002173 files, branch='master', tag='init')
2174
2175 return True
2176
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002177 def setupAllProjectKeys(self):
2178 if self.create_project_keys:
2179 return
2180
James E. Blair39840362017-06-23 20:34:02 +01002181 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002182 with open(os.path.join(FIXTURE_DIR, path)) as f:
2183 tenant_config = yaml.safe_load(f.read())
2184 for tenant in tenant_config:
2185 sources = tenant['tenant']['source']
2186 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002187 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002188 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002189 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002190 self.setupProjectKeys(source, project)
2191
2192 def setupProjectKeys(self, source, project):
2193 # Make sure we set up an RSA key for the project so that we
2194 # don't spend time generating one:
2195
James E. Blair6459db12017-06-29 14:57:20 -07002196 if isinstance(project, dict):
2197 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002198 key_root = os.path.join(self.state_root, 'keys')
2199 if not os.path.isdir(key_root):
2200 os.mkdir(key_root, 0o700)
2201 private_key_file = os.path.join(key_root, source, project + '.pem')
2202 private_key_dir = os.path.dirname(private_key_file)
2203 self.log.debug("Installing test keys for project %s at %s" % (
2204 project, private_key_file))
2205 if not os.path.isdir(private_key_dir):
2206 os.makedirs(private_key_dir)
2207 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2208 with open(private_key_file, 'w') as o:
2209 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002210
James E. Blair498059b2016-12-20 13:50:13 -08002211 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002212 self.zk_chroot_fixture = self.useFixture(
2213 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002214 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002215 self.zk_chroot_fixture.zookeeper_host,
2216 self.zk_chroot_fixture.zookeeper_port,
2217 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002218
James E. Blair96c6bf82016-01-15 16:20:40 -08002219 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002220 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002221
2222 files = {}
2223 for (dirpath, dirnames, filenames) in os.walk(source_path):
2224 for filename in filenames:
2225 test_tree_filepath = os.path.join(dirpath, filename)
2226 common_path = os.path.commonprefix([test_tree_filepath,
2227 source_path])
2228 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2229 with open(test_tree_filepath, 'r') as f:
2230 content = f.read()
2231 files[relative_filepath] = content
2232 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002233 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002234
James E. Blaire18d4602017-01-05 11:17:28 -08002235 def assertNodepoolState(self):
2236 # Make sure that there are no pending requests
2237
2238 requests = self.fake_nodepool.getNodeRequests()
2239 self.assertEqual(len(requests), 0)
2240
2241 nodes = self.fake_nodepool.getNodes()
2242 for node in nodes:
2243 self.assertFalse(node['_lock'], "Node %s is locked" %
2244 (node['_oid'],))
2245
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002246 def assertNoGeneratedKeys(self):
2247 # Make sure that Zuul did not generate any project keys
2248 # (unless it was supposed to).
2249
2250 if self.create_project_keys:
2251 return
2252
2253 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2254 test_key = i.read()
2255
2256 key_root = os.path.join(self.state_root, 'keys')
2257 for root, dirname, files in os.walk(key_root):
2258 for fn in files:
2259 with open(os.path.join(root, fn)) as f:
2260 self.assertEqual(test_key, f.read())
2261
Clark Boylanb640e052014-04-03 16:41:46 -07002262 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002263 self.log.debug("Assert final state")
2264 # Make sure no jobs are running
2265 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002266 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002267 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002268 gc.collect()
2269 for obj in gc.get_objects():
2270 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002271 self.log.debug("Leaked git repo object: 0x%x %s" %
2272 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002273 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002274 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002275 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002276 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002277 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002278 for tenant in self.sched.abide.tenants.values():
2279 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002280 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002281 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002282
2283 def shutdown(self):
2284 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002285 self.executor_server.hold_jobs_in_build = False
2286 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002287 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002288 self.merge_client.stop()
James E. Blairda5bb7e2018-01-22 16:12:17 -08002289 if self.merge_server:
2290 self.merge_server.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002291 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002292 self.sched.stop()
2293 self.sched.join()
2294 self.statsd.stop()
2295 self.statsd.join()
2296 self.webapp.stop()
2297 self.webapp.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002298 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002299 self.fake_nodepool.stop()
2300 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002301 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002302 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002303 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002304 # Further the pydevd threads also need to be whitelisted so debugging
2305 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002306 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002307 'pydevd.CommandThread',
2308 'pydevd.Reader',
2309 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002310 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002311 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002312 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002313 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002314 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002315 log_str = ""
2316 for thread_id, stack_frame in sys._current_frames().items():
2317 log_str += "Thread: %s\n" % thread_id
2318 log_str += "".join(traceback.format_stack(stack_frame))
2319 self.log.debug(log_str)
2320 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002321
James E. Blaira002b032017-04-18 10:35:48 -07002322 def assertCleanShutdown(self):
2323 pass
2324
James E. Blairc4ba97a2017-04-19 16:26:24 -07002325 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002326 parts = project.split('/')
2327 path = os.path.join(self.upstream_root, *parts[:-1])
2328 if not os.path.exists(path):
2329 os.makedirs(path)
2330 path = os.path.join(self.upstream_root, project)
2331 repo = git.Repo.init(path)
2332
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002333 with repo.config_writer() as config_writer:
2334 config_writer.set_value('user', 'email', 'user@example.com')
2335 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002336
Clark Boylanb640e052014-04-03 16:41:46 -07002337 repo.index.commit('initial commit')
2338 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002339 if tag:
2340 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002341
James E. Blair97d902e2014-08-21 13:25:56 -07002342 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002343 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002344 repo.git.clean('-x', '-f', '-d')
2345
James E. Blair97d902e2014-08-21 13:25:56 -07002346 def create_branch(self, project, branch):
2347 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002348 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002349 fn = os.path.join(path, 'README')
2350
2351 branch_head = repo.create_head(branch)
2352 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002353 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002354 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002355 f.close()
2356 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002357 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002358
James E. Blair97d902e2014-08-21 13:25:56 -07002359 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002360 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002361 repo.git.clean('-x', '-f', '-d')
2362
James E. Blairda5bb7e2018-01-22 16:12:17 -08002363 def delete_branch(self, project, branch):
2364 path = os.path.join(self.upstream_root, project)
2365 repo = git.Repo(path)
2366 repo.head.reference = repo.heads['master']
2367 zuul.merger.merger.reset_repo_to_head(repo)
2368 repo.delete_head(repo.heads[branch], force=True)
2369
Sachi King9f16d522016-03-16 12:20:45 +11002370 def create_commit(self, project):
2371 path = os.path.join(self.upstream_root, project)
2372 repo = git.Repo(path)
2373 repo.head.reference = repo.heads['master']
2374 file_name = os.path.join(path, 'README')
2375 with open(file_name, 'a') as f:
2376 f.write('creating fake commit\n')
2377 repo.index.add([file_name])
2378 commit = repo.index.commit('Creating a fake commit')
2379 return commit.hexsha
2380
James E. Blairf4a5f022017-04-18 14:01:10 -07002381 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002382 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002383 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002384 while len(self.builds):
2385 self.release(self.builds[0])
2386 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002387 i += 1
2388 if count is not None and i >= count:
2389 break
James E. Blairb8c16472015-05-05 14:55:26 -07002390
James E. Blairdf25ddc2017-07-08 07:57:09 -07002391 def getSortedBuilds(self):
2392 "Return the list of currently running builds sorted by name"
2393
2394 return sorted(self.builds, key=lambda x: x.name)
2395
Clark Boylanb640e052014-04-03 16:41:46 -07002396 def release(self, job):
2397 if isinstance(job, FakeBuild):
2398 job.release()
2399 else:
2400 job.waiting = False
2401 self.log.debug("Queued job %s released" % job.unique)
2402 self.gearman_server.wakeConnections()
2403
2404 def getParameter(self, job, name):
2405 if isinstance(job, FakeBuild):
2406 return job.parameters[name]
2407 else:
2408 parameters = json.loads(job.arguments)
2409 return parameters[name]
2410
Clark Boylanb640e052014-04-03 16:41:46 -07002411 def haveAllBuildsReported(self):
2412 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002413 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002414 return False
2415 # Find out if every build that the worker has completed has been
2416 # reported back to Zuul. If it hasn't then that means a Gearman
2417 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002418 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002419 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002420 if not zbuild:
2421 # It has already been reported
2422 continue
2423 # It hasn't been reported yet.
2424 return False
2425 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002426 worker = self.executor_server.executor_worker
2427 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002428 if connection.state == 'GRAB_WAIT':
2429 return False
2430 return True
2431
2432 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002433 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002434 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002435 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002436 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002437 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002438 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002439 for j in conn.related_jobs.values():
2440 if j.unique == build.uuid:
2441 client_job = j
2442 break
2443 if not client_job:
2444 self.log.debug("%s is not known to the gearman client" %
2445 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002446 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002447 if not client_job.handle:
2448 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002449 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002450 server_job = self.gearman_server.jobs.get(client_job.handle)
2451 if not server_job:
2452 self.log.debug("%s is not known to the gearman server" %
2453 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002454 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002455 if not hasattr(server_job, 'waiting'):
2456 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002457 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002458 if server_job.waiting:
2459 continue
James E. Blair17302972016-08-10 16:11:42 -07002460 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002461 self.log.debug("%s has not reported start" % build)
2462 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002463 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002464 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002465 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002466 if worker_build:
2467 if worker_build.isWaiting():
2468 continue
2469 else:
2470 self.log.debug("%s is running" % worker_build)
2471 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002472 else:
James E. Blair962220f2016-08-03 11:22:38 -07002473 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002474 return False
James E. Blaira002b032017-04-18 10:35:48 -07002475 for (build_uuid, job_worker) in \
2476 self.executor_server.job_workers.items():
2477 if build_uuid not in seen_builds:
2478 self.log.debug("%s is not finalized" % build_uuid)
2479 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002480 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002481
James E. Blairdce6cea2016-12-20 16:45:32 -08002482 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002483 if self.fake_nodepool.paused:
2484 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002485 if self.sched.nodepool.requests:
2486 return False
2487 return True
2488
James E. Blaira615c362017-10-02 17:34:42 -07002489 def areAllMergeJobsWaiting(self):
2490 for client_job in list(self.merge_client.jobs):
2491 if not client_job.handle:
2492 self.log.debug("%s has no handle" % client_job)
2493 return False
2494 server_job = self.gearman_server.jobs.get(client_job.handle)
2495 if not server_job:
2496 self.log.debug("%s is not known to the gearman server" %
2497 client_job)
2498 return False
2499 if not hasattr(server_job, 'waiting'):
2500 self.log.debug("%s is being enqueued" % server_job)
2501 return False
2502 if server_job.waiting:
2503 self.log.debug("%s is waiting" % server_job)
2504 continue
2505 self.log.debug("%s is not waiting" % server_job)
2506 return False
2507 return True
2508
Jan Hruban6b71aff2015-10-22 16:58:08 +02002509 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002510 for event_queue in self.event_queues:
2511 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002512
2513 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002514 for event_queue in self.event_queues:
2515 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002516
Clark Boylanb640e052014-04-03 16:41:46 -07002517 def waitUntilSettled(self):
2518 self.log.debug("Waiting until settled...")
2519 start = time.time()
2520 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002521 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002522 self.log.error("Timeout waiting for Zuul to settle")
2523 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002524 for event_queue in self.event_queues:
2525 self.log.error(" %s: %s" %
2526 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002527 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002528 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002529 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002530 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002531 self.log.error("All requests completed: %s" %
2532 (self.areAllNodeRequestsComplete(),))
2533 self.log.error("Merge client jobs: %s" %
2534 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002535 raise Exception("Timeout waiting for Zuul to settle")
2536 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002537
Paul Belanger174a8272017-03-14 13:20:10 -04002538 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002539 # have all build states propogated to zuul?
2540 if self.haveAllBuildsReported():
2541 # Join ensures that the queue is empty _and_ events have been
2542 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002543 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002544 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002545 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002546 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002547 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002548 self.areAllNodeRequestsComplete() and
2549 all(self.eventQueuesEmpty())):
2550 # The queue empty check is placed at the end to
2551 # ensure that if a component adds an event between
2552 # when locked the run handler and checked that the
2553 # components were stable, we don't erroneously
2554 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002555 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002556 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002557 self.log.debug("...settled.")
2558 return
2559 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002560 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002561 self.sched.wake_event.wait(0.1)
2562
2563 def countJobResults(self, jobs, result):
2564 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002565 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002566
Monty Taylor0d926122017-05-24 08:07:56 -05002567 def getBuildByName(self, name):
2568 for build in self.builds:
2569 if build.name == name:
2570 return build
2571 raise Exception("Unable to find build %s" % name)
2572
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002573 def assertJobNotInHistory(self, name, project=None):
2574 for job in self.history:
2575 if (project is None or
2576 job.parameters['zuul']['project']['name'] == project):
2577 self.assertNotEqual(job.name, name,
2578 'Job %s found in history' % name)
2579
James E. Blair96c6bf82016-01-15 16:20:40 -08002580 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002581 for job in self.history:
2582 if (job.name == name and
2583 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002584 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002585 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002586 raise Exception("Unable to find job %s in history" % name)
2587
2588 def assertEmptyQueues(self):
2589 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002590 for tenant in self.sched.abide.tenants.values():
2591 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002592 for pipeline_queue in pipeline.queues:
2593 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002594 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002595 pipeline.name, pipeline_queue.name,
2596 pipeline_queue.queue))
2597 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002598 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002599
2600 def assertReportedStat(self, key, value=None, kind=None):
2601 start = time.time()
2602 while time.time() < (start + 5):
2603 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002604 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002605 if key == k:
2606 if value is None and kind is None:
2607 return
2608 elif value:
2609 if value == v:
2610 return
2611 elif kind:
2612 if v.endswith('|' + kind):
2613 return
2614 time.sleep(0.1)
2615
Clark Boylanb640e052014-04-03 16:41:46 -07002616 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002617
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002618 def assertBuilds(self, builds):
2619 """Assert that the running builds are as described.
2620
2621 The list of running builds is examined and must match exactly
2622 the list of builds described by the input.
2623
2624 :arg list builds: A list of dictionaries. Each item in the
2625 list must match the corresponding build in the build
2626 history, and each element of the dictionary must match the
2627 corresponding attribute of the build.
2628
2629 """
James E. Blair3158e282016-08-19 09:34:11 -07002630 try:
2631 self.assertEqual(len(self.builds), len(builds))
2632 for i, d in enumerate(builds):
2633 for k, v in d.items():
2634 self.assertEqual(
2635 getattr(self.builds[i], k), v,
2636 "Element %i in builds does not match" % (i,))
2637 except Exception:
2638 for build in self.builds:
2639 self.log.error("Running build: %s" % build)
2640 else:
2641 self.log.error("No running builds")
2642 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002643
James E. Blairb536ecc2016-08-31 10:11:42 -07002644 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002645 """Assert that the completed builds are as described.
2646
2647 The list of completed builds is examined and must match
2648 exactly the list of builds described by the input.
2649
2650 :arg list history: A list of dictionaries. Each item in the
2651 list must match the corresponding build in the build
2652 history, and each element of the dictionary must match the
2653 corresponding attribute of the build.
2654
James E. Blairb536ecc2016-08-31 10:11:42 -07002655 :arg bool ordered: If true, the history must match the order
2656 supplied, if false, the builds are permitted to have
2657 arrived in any order.
2658
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002659 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002660 def matches(history_item, item):
2661 for k, v in item.items():
2662 if getattr(history_item, k) != v:
2663 return False
2664 return True
James E. Blair3158e282016-08-19 09:34:11 -07002665 try:
2666 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002667 if ordered:
2668 for i, d in enumerate(history):
2669 if not matches(self.history[i], d):
2670 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002671 "Element %i in history does not match %s" %
2672 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002673 else:
2674 unseen = self.history[:]
2675 for i, d in enumerate(history):
2676 found = False
2677 for unseen_item in unseen:
2678 if matches(unseen_item, d):
2679 found = True
2680 unseen.remove(unseen_item)
2681 break
2682 if not found:
2683 raise Exception("No match found for element %i "
2684 "in history" % (i,))
2685 if unseen:
2686 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002687 except Exception:
2688 for build in self.history:
2689 self.log.error("Completed build: %s" % build)
2690 else:
2691 self.log.error("No completed builds")
2692 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002693
James E. Blair6ac368c2016-12-22 18:07:20 -08002694 def printHistory(self):
2695 """Log the build history.
2696
2697 This can be useful during tests to summarize what jobs have
2698 completed.
2699
2700 """
2701 self.log.debug("Build history:")
2702 for build in self.history:
2703 self.log.debug(build)
2704
James E. Blair59fdbac2015-12-07 17:08:06 -08002705 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002706 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2707
James E. Blair9ea70072017-04-19 16:05:30 -07002708 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002709 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002710 if not os.path.exists(root):
2711 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002712 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2713 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002714- tenant:
2715 name: openstack
2716 source:
2717 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002718 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002719 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002720 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002721 - org/project
2722 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002723 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002724 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002725 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002726 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002727 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002728
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002729 def addTagToRepo(self, project, name, sha):
2730 path = os.path.join(self.upstream_root, project)
2731 repo = git.Repo(path)
2732 repo.git.tag(name, sha)
2733
2734 def delTagFromRepo(self, project, name):
2735 path = os.path.join(self.upstream_root, project)
2736 repo = git.Repo(path)
2737 repo.git.tag('-d', name)
2738
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002739 def addCommitToRepo(self, project, message, files,
2740 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002741 path = os.path.join(self.upstream_root, project)
2742 repo = git.Repo(path)
2743 repo.head.reference = branch
2744 zuul.merger.merger.reset_repo_to_head(repo)
2745 for fn, content in files.items():
2746 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002747 try:
2748 os.makedirs(os.path.dirname(fn))
2749 except OSError:
2750 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002751 with open(fn, 'w') as f:
2752 f.write(content)
2753 repo.index.add([fn])
2754 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002755 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002756 repo.heads[branch].commit = commit
2757 repo.head.reference = branch
2758 repo.git.clean('-x', '-f', '-d')
2759 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002760 if tag:
2761 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002762 return before
2763
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002764 def commitConfigUpdate(self, project_name, source_name):
2765 """Commit an update to zuul.yaml
2766
2767 This overwrites the zuul.yaml in the specificed project with
2768 the contents specified.
2769
2770 :arg str project_name: The name of the project containing
2771 zuul.yaml (e.g., common-config)
2772
2773 :arg str source_name: The path to the file (underneath the
2774 test fixture directory) whose contents should be used to
2775 replace zuul.yaml.
2776 """
2777
2778 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002779 files = {}
2780 with open(source_path, 'r') as f:
2781 data = f.read()
2782 layout = yaml.safe_load(data)
2783 files['zuul.yaml'] = data
2784 for item in layout:
2785 if 'job' in item:
2786 jobname = item['job']['name']
2787 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002788 before = self.addCommitToRepo(
2789 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002790 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002791 return before
2792
Clint Byrum627ba362017-08-14 13:20:40 -07002793 def newTenantConfig(self, source_name):
2794 """ Use this to update the tenant config file in tests
2795
2796 This will update self.tenant_config_file to point to a temporary file
2797 for the duration of this particular test. The content of that file will
2798 be taken from FIXTURE_DIR/source_name
2799
2800 After the test the original value of self.tenant_config_file will be
2801 restored.
2802
2803 :arg str source_name: The path of the file under
2804 FIXTURE_DIR that will be used to populate the new tenant
2805 config file.
2806 """
2807 source_path = os.path.join(FIXTURE_DIR, source_name)
2808 orig_tenant_config_file = self.tenant_config_file
2809 with tempfile.NamedTemporaryFile(
2810 delete=False, mode='wb') as new_tenant_config:
2811 self.tenant_config_file = new_tenant_config.name
2812 with open(source_path, mode='rb') as source_tenant_config:
2813 new_tenant_config.write(source_tenant_config.read())
2814 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2815 self.setupAllProjectKeys()
2816 self.log.debug(
2817 'tenant_config_file = {}'.format(self.tenant_config_file))
2818
2819 def _restoreTenantConfig():
2820 self.log.debug(
2821 'restoring tenant_config_file = {}'.format(
2822 orig_tenant_config_file))
2823 os.unlink(self.tenant_config_file)
2824 self.tenant_config_file = orig_tenant_config_file
2825 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2826 self.addCleanup(_restoreTenantConfig)
2827
James E. Blair7fc8daa2016-08-08 15:37:15 -07002828 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002829
James E. Blair7fc8daa2016-08-08 15:37:15 -07002830 """Inject a Fake (Gerrit) event.
2831
2832 This method accepts a JSON-encoded event and simulates Zuul
2833 having received it from Gerrit. It could (and should)
2834 eventually apply to any connection type, but is currently only
2835 used with Gerrit connections. The name of the connection is
2836 used to look up the corresponding server, and the event is
2837 simulated as having been received by all Zuul connections
2838 attached to that server. So if two Gerrit connections in Zuul
2839 are connected to the same Gerrit server, and you invoke this
2840 method specifying the name of one of them, the event will be
2841 received by both.
2842
2843 .. note::
2844
2845 "self.fake_gerrit.addEvent" calls should be migrated to
2846 this method.
2847
2848 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002849 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002850 :arg str event: The JSON-encoded event.
2851
2852 """
2853 specified_conn = self.connections.connections[connection]
2854 for conn in self.connections.connections.values():
2855 if (isinstance(conn, specified_conn.__class__) and
2856 specified_conn.server == conn.server):
2857 conn.addEvent(event)
2858
James E. Blaird8af5422017-05-24 13:59:40 -07002859 def getUpstreamRepos(self, projects):
2860 """Return upstream git repo objects for the listed projects
2861
2862 :arg list projects: A list of strings, each the canonical name
2863 of a project.
2864
2865 :returns: A dictionary of {name: repo} for every listed
2866 project.
2867 :rtype: dict
2868
2869 """
2870
2871 repos = {}
2872 for project in projects:
2873 # FIXME(jeblair): the upstream root does not yet have a
2874 # hostname component; that needs to be added, and this
2875 # line removed:
2876 tmp_project_name = '/'.join(project.split('/')[1:])
2877 path = os.path.join(self.upstream_root, tmp_project_name)
2878 repo = git.Repo(path)
2879 repos[project] = repo
2880 return repos
2881
James E. Blair3f876d52016-07-22 13:07:14 -07002882
2883class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002884 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002885 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002886
Jamie Lennox7655b552017-03-17 12:33:38 +11002887 @contextmanager
2888 def jobLog(self, build):
2889 """Print job logs on assertion errors
2890
2891 This method is a context manager which, if it encounters an
2892 ecxeption, adds the build log to the debug output.
2893
2894 :arg Build build: The build that's being asserted.
2895 """
2896 try:
2897 yield
2898 except Exception:
2899 path = os.path.join(self.test_root, build.uuid,
2900 'work', 'logs', 'job-output.txt')
2901 with open(path) as f:
2902 self.log.debug(f.read())
2903 raise
2904
Joshua Heskethd78b4482015-09-14 16:56:34 -06002905
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002906class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002907 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002908 use_ssl = True
2909
2910
Joshua Heskethd78b4482015-09-14 16:56:34 -06002911class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002912 def setup_config(self):
2913 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002914 for section_name in self.config.sections():
2915 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2916 section_name, re.I)
2917 if not con_match:
2918 continue
2919
2920 if self.config.get(section_name, 'driver') == 'sql':
2921 f = MySQLSchemaFixture()
2922 self.useFixture(f)
2923 if (self.config.get(section_name, 'dburi') ==
2924 '$MYSQL_FIXTURE_DBURI$'):
2925 self.config.set(section_name, 'dburi', f.dburi)