blob: 9a8878e0beecfafcae1618118b806762de5ffd60 [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 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080051import testtools.content
52import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080053from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000054import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070055
James E. Blaire511d2f2016-12-08 15:22:26 -080056import zuul.driver.gerrit.gerritsource as gerritsource
57import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070058import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.scheduler
60import zuul.webapp
Paul Belanger174a8272017-03-14 13:20:10 -040061import zuul.executor.server
62import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080063import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070065import zuul.merger.merger
66import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020067import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070068import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080069import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070070import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010071from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070072
73FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
74 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080075
76KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070077
Clark Boylanb640e052014-04-03 16:41:46 -070078
79def repack_repo(path):
80 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
81 output = subprocess.Popen(cmd, close_fds=True,
82 stdout=subprocess.PIPE,
83 stderr=subprocess.PIPE)
84 out = output.communicate()
85 if output.returncode:
86 raise Exception("git repack returned %d" % output.returncode)
87 return out
88
89
90def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040091 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070092
93
James E. Blaira190f3b2015-01-05 14:56:54 -080094def iterate_timeout(max_seconds, purpose):
95 start = time.time()
96 count = 0
97 while (time.time() < start + max_seconds):
98 count += 1
99 yield count
100 time.sleep(0)
101 raise Exception("Timeout waiting for %s" % purpose)
102
103
Jesse Keating436a5452017-04-20 11:48:41 -0700104def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700105 """Specify a layout file for use by a test method.
106
107 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700108 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700109
110 Some tests require only a very simple configuration. For those,
111 establishing a complete config directory hierachy is too much
112 work. In those cases, you can add a simple zuul.yaml file to the
113 test fixtures directory (in fixtures/layouts/foo.yaml) and use
114 this decorator to indicate the test method should use that rather
115 than the tenant config file specified by the test class.
116
117 The decorator will cause that layout file to be added to a
118 config-project called "common-config" and each "project" instance
119 referenced in the layout file will have a git repo automatically
120 initialized.
121 """
122
123 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700124 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700125 return test
126 return decorator
127
128
Gregory Haynes4fc12542015-04-22 20:38:06 -0700129class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700130 _common_path_default = "refs/changes"
131 _points_to_commits_only = True
132
133
Gregory Haynes4fc12542015-04-22 20:38:06 -0700134class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200135 categories = {'Approved': ('Approved', -1, 1),
136 'Code-Review': ('Code-Review', -2, 2),
137 'Verified': ('Verified', -2, 2)}
138
Clark Boylanb640e052014-04-03 16:41:46 -0700139 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700140 status='NEW', upstream_root=None, files={},
141 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700176 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair289f5932017-07-27 15:02:29 -0700180 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700183 if parent is None:
184 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700185 ref = GerritChangeReference.create(
186 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700187 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700189 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.git.clean('-x', '-f', '-d')
191
192 path = os.path.join(self.upstream_root, self.project)
193 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for fn, content in files.items():
195 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700196 if content is None:
197 os.unlink(fn)
198 repo.index.remove([fn])
199 else:
200 d = os.path.dirname(fn)
201 if not os.path.exists(d):
202 os.makedirs(d)
203 with open(fn, 'w') as f:
204 f.write(content)
205 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700206 else:
207 for fni in range(100):
208 fn = os.path.join(path, str(fni))
209 f = open(fn, 'w')
210 for ci in range(4096):
211 f.write(random.choice(string.printable))
212 f.close()
213 repo.index.add([fn])
214
215 r = repo.index.commit(msg)
216 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700217 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700218 repo.git.clean('-x', '-f', '-d')
219 repo.heads['master'].checkout()
220 return r
221
James E. Blair289f5932017-07-27 15:02:29 -0700222 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700223 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700225 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 data = ("test %s %s %s\n" %
227 (self.branch, self.number, self.latest_patchset))
228 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700229 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700230 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700231 ps_files = [{'file': '/COMMIT_MSG',
232 'type': 'ADDED'},
233 {'file': 'README',
234 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700235 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700236 ps_files.append({'file': f, 'type': 'ADDED'})
237 d = {'approvals': [],
238 'createdOn': time.time(),
239 'files': ps_files,
240 'number': str(self.latest_patchset),
241 'ref': 'refs/changes/1/%s/%s' % (self.number,
242 self.latest_patchset),
243 'revision': c.hexsha,
244 'uploader': {'email': 'user@example.com',
245 'name': 'User name',
246 'username': 'user'}}
247 self.data['currentPatchSet'] = d
248 self.patchsets.append(d)
249 self.data['submitRecords'] = self.getSubmitRecords()
250
251 def getPatchsetCreatedEvent(self, patchset):
252 event = {"type": "patchset-created",
253 "change": {"project": self.project,
254 "branch": self.branch,
255 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
256 "number": str(self.number),
257 "subject": self.subject,
258 "owner": {"name": "User Name"},
259 "url": "https://hostname/3"},
260 "patchSet": self.patchsets[patchset - 1],
261 "uploader": {"name": "User Name"}}
262 return event
263
264 def getChangeRestoredEvent(self):
265 event = {"type": "change-restored",
266 "change": {"project": self.project,
267 "branch": self.branch,
268 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
269 "number": str(self.number),
270 "subject": self.subject,
271 "owner": {"name": "User Name"},
272 "url": "https://hostname/3"},
273 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100274 "patchSet": self.patchsets[-1],
275 "reason": ""}
276 return event
277
278 def getChangeAbandonedEvent(self):
279 event = {"type": "change-abandoned",
280 "change": {"project": self.project,
281 "branch": self.branch,
282 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
283 "number": str(self.number),
284 "subject": self.subject,
285 "owner": {"name": "User Name"},
286 "url": "https://hostname/3"},
287 "abandoner": {"name": "User Name"},
288 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700289 "reason": ""}
290 return event
291
292 def getChangeCommentEvent(self, patchset):
293 event = {"type": "comment-added",
294 "change": {"project": self.project,
295 "branch": self.branch,
296 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
297 "number": str(self.number),
298 "subject": self.subject,
299 "owner": {"name": "User Name"},
300 "url": "https://hostname/3"},
301 "patchSet": self.patchsets[patchset - 1],
302 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200303 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700304 "description": "Code-Review",
305 "value": "0"}],
306 "comment": "This is a comment"}
307 return event
308
James E. Blairc2a5ed72017-02-20 14:12:01 -0500309 def getChangeMergedEvent(self):
310 event = {"submitter": {"name": "Jenkins",
311 "username": "jenkins"},
312 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
313 "patchSet": self.patchsets[-1],
314 "change": self.data,
315 "type": "change-merged",
316 "eventCreatedOn": 1487613810}
317 return event
318
James E. Blair8cce42e2016-10-18 08:18:36 -0700319 def getRefUpdatedEvent(self):
320 path = os.path.join(self.upstream_root, self.project)
321 repo = git.Repo(path)
322 oldrev = repo.heads[self.branch].commit.hexsha
323
324 event = {
325 "type": "ref-updated",
326 "submitter": {
327 "name": "User Name",
328 },
329 "refUpdate": {
330 "oldRev": oldrev,
331 "newRev": self.patchsets[-1]['revision'],
332 "refName": self.branch,
333 "project": self.project,
334 }
335 }
336 return event
337
Joshua Hesketh642824b2014-07-01 17:54:59 +1000338 def addApproval(self, category, value, username='reviewer_john',
339 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700340 if not granted_on:
341 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000342 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200343 'description': self.categories[category][0],
344 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000345 'value': str(value),
346 'by': {
347 'username': username,
348 'email': username + '@example.com',
349 },
350 'grantedOn': int(granted_on)
351 }
Clark Boylanb640e052014-04-03 16:41:46 -0700352 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200353 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700354 del self.patchsets[-1]['approvals'][i]
355 self.patchsets[-1]['approvals'].append(approval)
356 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000357 'author': {'email': 'author@example.com',
358 'name': 'Patchset Author',
359 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'change': {'branch': self.branch,
361 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
362 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000363 'owner': {'email': 'owner@example.com',
364 'name': 'Change Owner',
365 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700366 'project': self.project,
367 'subject': self.subject,
368 'topic': 'master',
369 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000370 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700371 'patchSet': self.patchsets[-1],
372 'type': 'comment-added'}
373 self.data['submitRecords'] = self.getSubmitRecords()
374 return json.loads(json.dumps(event))
375
376 def getSubmitRecords(self):
377 status = {}
378 for cat in self.categories.keys():
379 status[cat] = 0
380
381 for a in self.patchsets[-1]['approvals']:
382 cur = status[a['type']]
383 cat_min, cat_max = self.categories[a['type']][1:]
384 new = int(a['value'])
385 if new == cat_min:
386 cur = new
387 elif abs(new) > abs(cur):
388 cur = new
389 status[a['type']] = cur
390
391 labels = []
392 ok = True
393 for typ, cat in self.categories.items():
394 cur = status[typ]
395 cat_min, cat_max = cat[1:]
396 if cur == cat_min:
397 value = 'REJECT'
398 ok = False
399 elif cur == cat_max:
400 value = 'OK'
401 else:
402 value = 'NEED'
403 ok = False
404 labels.append({'label': cat[0], 'status': value})
405 if ok:
406 return [{'status': 'OK'}]
407 return [{'status': 'NOT_READY',
408 'labels': labels}]
409
410 def setDependsOn(self, other, patchset):
411 self.depends_on_change = other
412 d = {'id': other.data['id'],
413 'number': other.data['number'],
414 'ref': other.patchsets[patchset - 1]['ref']
415 }
416 self.data['dependsOn'] = [d]
417
418 other.needed_by_changes.append(self)
419 needed = other.data.get('neededBy', [])
420 d = {'id': self.data['id'],
421 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700422 'ref': self.patchsets[-1]['ref'],
423 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700424 }
425 needed.append(d)
426 other.data['neededBy'] = needed
427
428 def query(self):
429 self.queried += 1
430 d = self.data.get('dependsOn')
431 if d:
432 d = d[0]
433 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
434 d['isCurrentPatchSet'] = True
435 else:
436 d['isCurrentPatchSet'] = False
437 return json.loads(json.dumps(self.data))
438
439 def setMerged(self):
440 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000441 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700442 return
443 if self.fail_merge:
444 return
445 self.data['status'] = 'MERGED'
446 self.open = False
447
448 path = os.path.join(self.upstream_root, self.project)
449 repo = git.Repo(path)
450 repo.heads[self.branch].commit = \
451 repo.commit(self.patchsets[-1]['revision'])
452
453 def setReported(self):
454 self.reported += 1
455
456
James E. Blaire511d2f2016-12-08 15:22:26 -0800457class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700458 """A Fake Gerrit connection for use in tests.
459
460 This subclasses
461 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
462 ability for tests to add changes to the fake Gerrit it represents.
463 """
464
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700466
James E. Blaire511d2f2016-12-08 15:22:26 -0800467 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700468 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800469 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 connection_config)
471
Monty Taylorb934c1a2017-06-16 19:31:47 -0500472 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
474 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000475 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700476 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200477 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700478
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700479 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700480 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700481 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700482 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700483 c = FakeGerritChange(self, self.change_number, project, branch,
484 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700485 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700486 self.changes[self.change_number] = c
487 return c
488
James E. Blair1edfd972017-12-01 15:54:24 -0800489 def addFakeTag(self, project, branch, tag):
490 path = os.path.join(self.upstream_root, project)
491 repo = git.Repo(path)
492 commit = repo.heads[branch].commit
493 newrev = commit.hexsha
494 ref = 'refs/tags/' + tag
495
496 git.Tag.create(repo, tag, commit)
497
498 event = {
499 "type": "ref-updated",
500 "submitter": {
501 "name": "User Name",
502 },
503 "refUpdate": {
504 "oldRev": 40 * '0',
505 "newRev": newrev,
506 "refName": ref,
507 "project": project,
508 }
509 }
510 return event
511
James E. Blair72facdc2017-08-17 10:29:12 -0700512 def getFakeBranchCreatedEvent(self, project, branch):
513 path = os.path.join(self.upstream_root, project)
514 repo = git.Repo(path)
515 oldrev = 40 * '0'
516
517 event = {
518 "type": "ref-updated",
519 "submitter": {
520 "name": "User Name",
521 },
522 "refUpdate": {
523 "oldRev": oldrev,
524 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700525 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700526 "project": project,
527 }
528 }
529 return event
530
Clark Boylanb640e052014-04-03 16:41:46 -0700531 def review(self, project, changeid, message, action):
532 number, ps = changeid.split(',')
533 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000534
535 # Add the approval back onto the change (ie simulate what gerrit would
536 # do).
537 # Usually when zuul leaves a review it'll create a feedback loop where
538 # zuul's review enters another gerrit event (which is then picked up by
539 # zuul). However, we can't mimic this behaviour (by adding this
540 # approval event into the queue) as it stops jobs from checking what
541 # happens before this event is triggered. If a job needs to see what
542 # happens they can add their own verified event into the queue.
543 # Nevertheless, we can update change with the new review in gerrit.
544
James E. Blair8b5408c2016-08-08 15:37:46 -0700545 for cat in action.keys():
546 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000547 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000548
Clark Boylanb640e052014-04-03 16:41:46 -0700549 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000550
Clark Boylanb640e052014-04-03 16:41:46 -0700551 if 'submit' in action:
552 change.setMerged()
553 if message:
554 change.setReported()
555
556 def query(self, number):
557 change = self.changes.get(int(number))
558 if change:
559 return change.query()
560 return {}
561
James E. Blairc494d542014-08-06 09:23:52 -0700562 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700563 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700564 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800565 if query.startswith('change:'):
566 # Query a specific changeid
567 changeid = query[len('change:'):]
568 l = [change.query() for change in self.changes.values()
569 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700570 elif query.startswith('message:'):
571 # Query the content of a commit message
572 msg = query[len('message:'):].strip()
573 l = [change.query() for change in self.changes.values()
574 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800575 else:
576 # Query all open changes
577 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700578 return l
James E. Blairc494d542014-08-06 09:23:52 -0700579
Joshua Hesketh352264b2015-08-11 23:42:08 +1000580 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700581 pass
582
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200583 def _uploadPack(self, project):
584 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
585 'multi_ack thin-pack side-band side-band-64k ofs-delta '
586 'shallow no-progress include-tag multi_ack_detailed no-done\n')
587 path = os.path.join(self.upstream_root, project.name)
588 repo = git.Repo(path)
589 for ref in repo.refs:
590 r = ref.object.hexsha + ' ' + ref.path + '\n'
591 ret += '%04x%s' % (len(r) + 4, r)
592 ret += '0000'
593 return ret
594
Joshua Hesketh352264b2015-08-11 23:42:08 +1000595 def getGitUrl(self, project):
596 return os.path.join(self.upstream_root, project.name)
597
Clark Boylanb640e052014-04-03 16:41:46 -0700598
Gregory Haynes4fc12542015-04-22 20:38:06 -0700599class GithubChangeReference(git.Reference):
600 _common_path_default = "refs/pull"
601 _points_to_commits_only = True
602
603
Tobias Henkel64e37a02017-08-02 10:13:30 +0200604class FakeGithub(object):
605
606 class FakeUser(object):
607 def __init__(self, login):
608 self.login = login
609 self.name = "Github User"
610 self.email = "github.user@example.com"
611
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200612 class FakeBranch(object):
613 def __init__(self, branch='master'):
614 self.name = branch
615
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200616 class FakeStatus(object):
617 def __init__(self, state, url, description, context, user):
618 self._state = state
619 self._url = url
620 self._description = description
621 self._context = context
622 self._user = user
623
624 def as_dict(self):
625 return {
626 'state': self._state,
627 'url': self._url,
628 'description': self._description,
629 'context': self._context,
630 'creator': {
631 'login': self._user
632 }
633 }
634
635 class FakeCommit(object):
636 def __init__(self):
637 self._statuses = []
638
639 def set_status(self, state, url, description, context, user):
640 status = FakeGithub.FakeStatus(
641 state, url, description, context, user)
642 # always insert a status to the front of the list, to represent
643 # the last status provided for a commit.
644 self._statuses.insert(0, status)
645
646 def statuses(self):
647 return self._statuses
648
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200649 class FakeRepository(object):
650 def __init__(self):
651 self._branches = [FakeGithub.FakeBranch()]
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200652 self._commits = {}
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200653
Tobias Henkeleca46202017-08-02 20:27:10 +0200654 def branches(self, protected=False):
655 if protected:
656 # simulate there is no protected branch
657 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200658 return self._branches
659
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200660 def create_status(self, sha, state, url, description, context,
661 user='zuul'):
662 # Since we're bypassing github API, which would require a user, we
663 # default the user as 'zuul' here.
664 commit = self._commits.get(sha, None)
665 if commit is None:
666 commit = FakeGithub.FakeCommit()
667 self._commits[sha] = commit
668 commit.set_status(state, url, description, context, user)
669
670 def commit(self, sha):
671 commit = self._commits.get(sha, None)
672 if commit is None:
673 commit = FakeGithub.FakeCommit()
674 self._commits[sha] = commit
675 return commit
676
James E. Blairf2c12d62018-01-05 10:28:09 -0800677 class FakeLabel(object):
678 def __init__(self, name):
679 self.name = name
680
681 class FakeIssue(object):
682 def __init__(self, fake_pull_request):
683 self._fake_pull_request = fake_pull_request
684
685 def pull_request(self):
686 return FakeGithub.FakePull(self._fake_pull_request)
687
688 def labels(self):
689 return [FakeGithub.FakeLabel(l)
690 for l in self._fake_pull_request.labels]
691
692 class FakeFile(object):
693 def __init__(self, filename):
694 self.filename = filename
695
696 class FakePull(object):
697 def __init__(self, fake_pull_request):
698 self._fake_pull_request = fake_pull_request
699
700 def issue(self):
701 return FakeGithub.FakeIssue(self._fake_pull_request)
702
703 def files(self):
704 return [FakeGithub.FakeFile(fn)
705 for fn in self._fake_pull_request.files]
706
707 def as_dict(self):
708 pr = self._fake_pull_request
709 connection = pr.github
710 data = {
711 'number': pr.number,
712 'title': pr.subject,
713 'url': 'https://%s/%s/pull/%s' % (
714 connection.server, pr.project, pr.number
715 ),
716 'updated_at': pr.updated_at,
717 'base': {
718 'repo': {
719 'full_name': pr.project
720 },
721 'ref': pr.branch,
722 },
723 'mergeable': True,
724 'state': pr.state,
725 'head': {
726 'sha': pr.head_sha,
727 'repo': {
728 'full_name': pr.project
729 }
730 },
731 'merged': pr.is_merged,
732 'body': pr.body
733 }
734 return data
735
736 class FakeIssueSearchResult(object):
737 def __init__(self, issue):
738 self.issue = issue
739
740 def __init__(self, connection):
741 self._fake_github_connection = connection
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200742 self._repos = {}
743
Tobias Henkel64e37a02017-08-02 10:13:30 +0200744 def user(self, login):
745 return self.FakeUser(login)
746
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200747 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200748 return self._repos.get((owner, proj), None)
749
750 def repo_from_project(self, project):
751 # This is a convenience method for the tests.
752 owner, proj = project.split('/')
753 return self.repository(owner, proj)
754
755 def addProject(self, project):
756 owner, proj = project.name.split('/')
757 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200758
James E. Blairf2c12d62018-01-05 10:28:09 -0800759 def pull_request(self, owner, project, number):
760 fake_pr = self._fake_github_connection.pull_requests[number - 1]
761 return self.FakePull(fake_pr)
762
763 def search_issues(self, query):
764 def tokenize(s):
765 return re.findall(r'[\w]+', s)
766
767 parts = tokenize(query)
768 terms = set()
769 results = []
770 for part in parts:
771 kv = part.split(':', 1)
772 if len(kv) == 2:
773 if kv[0] in set('type', 'is', 'in'):
774 # We only perform one search now and these aren't
775 # important; we can honor these terms later if
776 # necessary.
777 continue
778 terms.add(part)
779
780 for pr in self._fake_github_connection.pull_requests:
781 if not pr.body:
782 body = set()
783 else:
784 body = set(tokenize(pr.body))
785 if terms.intersection(body):
786 issue = FakeGithub.FakeIssue(pr)
787 results.append(FakeGithub.FakeIssueSearchResult(issue))
788
789 return results
790
Tobias Henkel64e37a02017-08-02 10:13:30 +0200791
Gregory Haynes4fc12542015-04-22 20:38:06 -0700792class FakeGithubPullRequest(object):
793
794 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800795 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700796 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700797 """Creates a new PR with several commits.
798 Sends an event about opened PR."""
799 self.github = github
800 self.source = github
801 self.number = number
802 self.project = project
803 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100804 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700805 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100806 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700807 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100808 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700809 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100810 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100811 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800812 self.reviews = []
813 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814 self.updated_at = None
815 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100816 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100817 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700818 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700819 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100820 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700821 self._updateTimeStamp()
822
Jan Hruban570d01c2016-03-10 21:51:32 +0100823 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700824 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100825 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700826 self._updateTimeStamp()
827
Jan Hruban570d01c2016-03-10 21:51:32 +0100828 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700829 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100830 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700831 self._updateTimeStamp()
832
833 def getPullRequestOpenedEvent(self):
834 return self._getPullRequestEvent('opened')
835
836 def getPullRequestSynchronizeEvent(self):
837 return self._getPullRequestEvent('synchronize')
838
839 def getPullRequestReopenedEvent(self):
840 return self._getPullRequestEvent('reopened')
841
842 def getPullRequestClosedEvent(self):
843 return self._getPullRequestEvent('closed')
844
Jesse Keatinga41566f2017-06-14 18:17:51 -0700845 def getPullRequestEditedEvent(self):
846 return self._getPullRequestEvent('edited')
847
Gregory Haynes4fc12542015-04-22 20:38:06 -0700848 def addComment(self, message):
849 self.comments.append(message)
850 self._updateTimeStamp()
851
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200852 def getCommentAddedEvent(self, text):
853 name = 'issue_comment'
854 data = {
855 'action': 'created',
856 'issue': {
857 'number': self.number
858 },
859 'comment': {
860 'body': text
861 },
862 'repository': {
863 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100864 },
865 'sender': {
866 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200867 }
868 }
869 return (name, data)
870
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800871 def getReviewAddedEvent(self, review):
872 name = 'pull_request_review'
873 data = {
874 'action': 'submitted',
875 'pull_request': {
876 'number': self.number,
877 'title': self.subject,
878 'updated_at': self.updated_at,
879 'base': {
880 'ref': self.branch,
881 'repo': {
882 'full_name': self.project
883 }
884 },
885 'head': {
886 'sha': self.head_sha
887 }
888 },
889 'review': {
890 'state': review
891 },
892 'repository': {
893 'full_name': self.project
894 },
895 'sender': {
896 'login': 'ghuser'
897 }
898 }
899 return (name, data)
900
Jan Hruban16ad31f2015-11-07 14:39:07 +0100901 def addLabel(self, name):
902 if name not in self.labels:
903 self.labels.append(name)
904 self._updateTimeStamp()
905 return self._getLabelEvent(name)
906
907 def removeLabel(self, name):
908 if name in self.labels:
909 self.labels.remove(name)
910 self._updateTimeStamp()
911 return self._getUnlabelEvent(name)
912
913 def _getLabelEvent(self, label):
914 name = 'pull_request'
915 data = {
916 'action': 'labeled',
917 'pull_request': {
918 'number': self.number,
919 'updated_at': self.updated_at,
920 'base': {
921 'ref': self.branch,
922 'repo': {
923 'full_name': self.project
924 }
925 },
926 'head': {
927 'sha': self.head_sha
928 }
929 },
930 'label': {
931 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100932 },
933 'sender': {
934 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100935 }
936 }
937 return (name, data)
938
939 def _getUnlabelEvent(self, label):
940 name = 'pull_request'
941 data = {
942 'action': 'unlabeled',
943 'pull_request': {
944 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100945 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100946 'updated_at': self.updated_at,
947 'base': {
948 'ref': self.branch,
949 'repo': {
950 'full_name': self.project
951 }
952 },
953 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800954 'sha': self.head_sha,
955 'repo': {
956 'full_name': self.project
957 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100958 }
959 },
960 'label': {
961 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100962 },
963 'sender': {
964 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100965 }
966 }
967 return (name, data)
968
Jesse Keatinga41566f2017-06-14 18:17:51 -0700969 def editBody(self, body):
970 self.body = body
971 self._updateTimeStamp()
972
Gregory Haynes4fc12542015-04-22 20:38:06 -0700973 def _getRepo(self):
974 repo_path = os.path.join(self.upstream_root, self.project)
975 return git.Repo(repo_path)
976
977 def _createPRRef(self):
978 repo = self._getRepo()
979 GithubChangeReference.create(
980 repo, self._getPRReference(), 'refs/tags/init')
981
Jan Hruban570d01c2016-03-10 21:51:32 +0100982 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700983 repo = self._getRepo()
984 ref = repo.references[self._getPRReference()]
985 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100986 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700987 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100988 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700989 repo.head.reference = ref
990 zuul.merger.merger.reset_repo_to_head(repo)
991 repo.git.clean('-x', '-f', '-d')
992
Jan Hruban570d01c2016-03-10 21:51:32 +0100993 if files:
994 fn = files[0]
995 self.files = files
996 else:
997 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
998 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100999 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001000 fn = os.path.join(repo.working_dir, fn)
1001 f = open(fn, 'w')
1002 with open(fn, 'w') as f:
1003 f.write("test %s %s\n" %
1004 (self.branch, self.number))
1005 repo.index.add([fn])
1006
1007 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -08001008 # Create an empty set of statuses for the given sha,
1009 # each sha on a PR may have a status set on it
1010 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -07001011 repo.head.reference = 'master'
1012 zuul.merger.merger.reset_repo_to_head(repo)
1013 repo.git.clean('-x', '-f', '-d')
1014 repo.heads['master'].checkout()
1015
1016 def _updateTimeStamp(self):
1017 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
1018
1019 def getPRHeadSha(self):
1020 repo = self._getRepo()
1021 return repo.references[self._getPRReference()].commit.hexsha
1022
Jesse Keatingae4cd272017-01-30 17:10:44 -08001023 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -08001024 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
1025 # convert the timestamp to a str format that would be returned
1026 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -08001027
Adam Gandelmand81dd762017-02-09 15:15:49 -08001028 if granted_on:
1029 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
1030 submitted_at = time.strftime(
1031 gh_time_format, granted_on.timetuple())
1032 else:
1033 # github timestamps only down to the second, so we need to make
1034 # sure reviews that tests add appear to be added over a period of
1035 # time in the past and not all at once.
1036 if not self.reviews:
1037 # the first review happens 10 mins ago
1038 offset = 600
1039 else:
1040 # subsequent reviews happen 1 minute closer to now
1041 offset = 600 - (len(self.reviews) * 60)
1042
1043 granted_on = datetime.datetime.utcfromtimestamp(
1044 time.time() - offset)
1045 submitted_at = time.strftime(
1046 gh_time_format, granted_on.timetuple())
1047
Jesse Keatingae4cd272017-01-30 17:10:44 -08001048 self.reviews.append({
1049 'state': state,
1050 'user': {
1051 'login': user,
1052 'email': user + "@derp.com",
1053 },
Adam Gandelmand81dd762017-02-09 15:15:49 -08001054 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -08001055 })
1056
Gregory Haynes4fc12542015-04-22 20:38:06 -07001057 def _getPRReference(self):
1058 return '%s/head' % self.number
1059
1060 def _getPullRequestEvent(self, action):
1061 name = 'pull_request'
1062 data = {
1063 'action': action,
1064 'number': self.number,
1065 'pull_request': {
1066 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +01001067 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -07001068 'updated_at': self.updated_at,
1069 'base': {
1070 'ref': self.branch,
1071 'repo': {
1072 'full_name': self.project
1073 }
1074 },
1075 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001076 'sha': self.head_sha,
1077 'repo': {
1078 'full_name': self.project
1079 }
Jesse Keatinga41566f2017-06-14 18:17:51 -07001080 },
1081 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +01001082 },
1083 'sender': {
1084 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -07001085 }
1086 }
1087 return (name, data)
1088
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001089 def getCommitStatusEvent(self, context, state='success', user='zuul'):
1090 name = 'status'
1091 data = {
1092 'state': state,
1093 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -07001094 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001095 'description': 'Test results for %s: %s' % (self.head_sha, state),
1096 'target_url': 'http://zuul/%s' % self.head_sha,
1097 'branches': [],
1098 'context': context,
1099 'sender': {
1100 'login': user
1101 }
1102 }
1103 return (name, data)
1104
James E. Blair289f5932017-07-27 15:02:29 -07001105 def setMerged(self, commit_message):
1106 self.is_merged = True
1107 self.merge_message = commit_message
1108
1109 repo = self._getRepo()
1110 repo.heads[self.branch].commit = repo.commit(self.head_sha)
1111
Gregory Haynes4fc12542015-04-22 20:38:06 -07001112
1113class FakeGithubConnection(githubconnection.GithubConnection):
1114 log = logging.getLogger("zuul.test.FakeGithubConnection")
1115
1116 def __init__(self, driver, connection_name, connection_config,
1117 upstream_root=None):
1118 super(FakeGithubConnection, self).__init__(driver, connection_name,
1119 connection_config)
1120 self.connection_name = connection_name
1121 self.pr_number = 0
1122 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001123 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001124 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001125 self.merge_failure = False
1126 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001127 self.reports = []
James E. Blairf2c12d62018-01-05 10:28:09 -08001128 self.github_client = FakeGithub(self)
Tobias Henkel64e37a02017-08-02 10:13:30 +02001129
1130 def getGithubClient(self,
1131 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -06001132 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +02001133 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001134
Jesse Keatinga41566f2017-06-14 18:17:51 -07001135 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001136 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001137 self.pr_number += 1
1138 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001139 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001140 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001141 self.pull_requests.append(pull_request)
1142 return pull_request
1143
Jesse Keating71a47ff2017-06-06 11:36:43 -07001144 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1145 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001146 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001147 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001148 if not new_rev:
1149 new_rev = random_sha1()
1150 name = 'push'
1151 data = {
1152 'ref': ref,
1153 'before': old_rev,
1154 'after': new_rev,
1155 'repository': {
1156 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001157 },
1158 'commits': [
1159 {
1160 'added': added_files,
1161 'removed': removed_files,
1162 'modified': modified_files
1163 }
1164 ]
Wayne1a78c612015-06-11 17:14:13 -07001165 }
1166 return (name, data)
1167
Gregory Haynes4fc12542015-04-22 20:38:06 -07001168 def emitEvent(self, event):
1169 """Emulates sending the GitHub webhook event to the connection."""
1170 port = self.webapp.server.socket.getsockname()[1]
1171 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001172 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001173 secret = self.connection_config['webhook_token']
1174 signature = githubconnection._sign_request(payload, secret)
1175 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001176 req = urllib.request.Request(
1177 'http://localhost:%s/connection/%s/payload'
1178 % (port, self.connection_name),
1179 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001180 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001181
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001182 def addProject(self, project):
1183 # use the original method here and additionally register it in the
1184 # fake github
1185 super(FakeGithubConnection, self).addProject(project)
1186 self.getGithubClient(project).addProject(project)
1187
Jesse Keating9021a012017-08-29 14:45:27 -07001188 def getPullBySha(self, sha, project):
1189 prs = list(set([p for p in self.pull_requests if
1190 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001191 if len(prs) > 1:
1192 raise Exception('Multiple pulls found with head sha: %s' % sha)
1193 pr = prs[0]
1194 return self.getPull(pr.project, pr.number)
1195
Jesse Keatingae4cd272017-01-30 17:10:44 -08001196 def _getPullReviews(self, owner, project, number):
1197 pr = self.pull_requests[number - 1]
1198 return pr.reviews
1199
Jesse Keatingae4cd272017-01-30 17:10:44 -08001200 def getRepoPermission(self, project, login):
1201 owner, proj = project.split('/')
1202 for pr in self.pull_requests:
1203 pr_owner, pr_project = pr.project.split('/')
1204 if (pr_owner == owner and proj == pr_project):
1205 if login in pr.writers:
1206 return 'write'
1207 else:
1208 return 'read'
1209
Gregory Haynes4fc12542015-04-22 20:38:06 -07001210 def getGitUrl(self, project):
1211 return os.path.join(self.upstream_root, str(project))
1212
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001213 def real_getGitUrl(self, project):
1214 return super(FakeGithubConnection, self).getGitUrl(project)
1215
Jan Hrubane252a732017-01-03 15:03:09 +01001216 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001217 # record that this got reported
1218 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001219 pull_request = self.pull_requests[pr_number - 1]
1220 pull_request.addComment(message)
1221
Jan Hruban3b415922016-02-03 13:10:22 +01001222 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001223 # record that this got reported
1224 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001225 pull_request = self.pull_requests[pr_number - 1]
1226 if self.merge_failure:
1227 raise Exception('Pull request was not merged')
1228 if self.merge_not_allowed_count > 0:
1229 self.merge_not_allowed_count -= 1
1230 raise MergeFailure('Merge was not successful due to mergeability'
1231 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001232 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001233
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001234 def setCommitStatus(self, project, sha, state, url='', description='',
1235 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001236 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001237 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001238 super(FakeGithubConnection, self).setCommitStatus(
1239 project, sha, state,
1240 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001241
Jan Hruban16ad31f2015-11-07 14:39:07 +01001242 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001243 # record that this got reported
1244 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001245 pull_request = self.pull_requests[pr_number - 1]
1246 pull_request.addLabel(label)
1247
1248 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001249 # record that this got reported
1250 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001251 pull_request = self.pull_requests[pr_number - 1]
1252 pull_request.removeLabel(label)
1253
Gregory Haynes4fc12542015-04-22 20:38:06 -07001254
Clark Boylanb640e052014-04-03 16:41:46 -07001255class BuildHistory(object):
1256 def __init__(self, **kw):
1257 self.__dict__.update(kw)
1258
1259 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001260 return ("<Completed build, result: %s name: %s uuid: %s "
1261 "changes: %s ref: %s>" %
1262 (self.result, self.name, self.uuid,
1263 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001264
1265
Clark Boylanb640e052014-04-03 16:41:46 -07001266class FakeStatsd(threading.Thread):
1267 def __init__(self):
1268 threading.Thread.__init__(self)
1269 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001270 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001271 self.sock.bind(('', 0))
1272 self.port = self.sock.getsockname()[1]
1273 self.wake_read, self.wake_write = os.pipe()
1274 self.stats = []
1275
1276 def run(self):
1277 while True:
1278 poll = select.poll()
1279 poll.register(self.sock, select.POLLIN)
1280 poll.register(self.wake_read, select.POLLIN)
1281 ret = poll.poll()
1282 for (fd, event) in ret:
1283 if fd == self.sock.fileno():
1284 data = self.sock.recvfrom(1024)
1285 if not data:
1286 return
1287 self.stats.append(data[0])
1288 if fd == self.wake_read:
1289 return
1290
1291 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001292 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001293
1294
James E. Blaire1767bc2016-08-02 10:00:27 -07001295class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001296 log = logging.getLogger("zuul.test")
1297
Paul Belanger174a8272017-03-14 13:20:10 -04001298 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001299 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001300 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001301 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001302 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001303 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001304 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001305 # TODOv3(jeblair): self.node is really "the label of the node
1306 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001307 # keep using it like this, or we may end up exposing more of
1308 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001309 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001310 self.node = None
1311 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001312 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001313 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001314 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001315 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001316 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001317 self.wait_condition = threading.Condition()
1318 self.waiting = False
1319 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001320 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001321 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001322 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001323 items = self.parameters['zuul']['items']
1324 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1325 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001326
James E. Blair3158e282016-08-19 09:34:11 -07001327 def __repr__(self):
1328 waiting = ''
1329 if self.waiting:
1330 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001331 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1332 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001333
Clark Boylanb640e052014-04-03 16:41:46 -07001334 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001335 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001336 self.wait_condition.acquire()
1337 self.wait_condition.notify()
1338 self.waiting = False
1339 self.log.debug("Build %s released" % self.unique)
1340 self.wait_condition.release()
1341
1342 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001343 """Return whether this build is being held.
1344
1345 :returns: Whether the build is being held.
1346 :rtype: bool
1347 """
1348
Clark Boylanb640e052014-04-03 16:41:46 -07001349 self.wait_condition.acquire()
1350 if self.waiting:
1351 ret = True
1352 else:
1353 ret = False
1354 self.wait_condition.release()
1355 return ret
1356
1357 def _wait(self):
1358 self.wait_condition.acquire()
1359 self.waiting = True
1360 self.log.debug("Build %s waiting" % self.unique)
1361 self.wait_condition.wait()
1362 self.wait_condition.release()
1363
1364 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001365 self.log.debug('Running build %s' % self.unique)
1366
Paul Belanger174a8272017-03-14 13:20:10 -04001367 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001368 self.log.debug('Holding build %s' % self.unique)
1369 self._wait()
1370 self.log.debug("Build %s continuing" % self.unique)
1371
James E. Blair412fba82017-01-26 15:00:50 -08001372 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001373 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001374 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001375 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001376 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001377 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001378 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001379
James E. Blaire1767bc2016-08-02 10:00:27 -07001380 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001381
James E. Blaira5dba232016-08-08 15:53:24 -07001382 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001383 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001384 for change in changes:
1385 if self.hasChanges(change):
1386 return True
1387 return False
1388
James E. Blaire7b99a02016-08-05 14:27:34 -07001389 def hasChanges(self, *changes):
1390 """Return whether this build has certain changes in its git repos.
1391
1392 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001393 are expected to be present (in order) in the git repository of
1394 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001395
1396 :returns: Whether the build has the indicated changes.
1397 :rtype: bool
1398
1399 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001400 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001401 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001402 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001403 try:
1404 repo = git.Repo(path)
1405 except NoSuchPathError as e:
1406 self.log.debug('%s' % e)
1407 return False
James E. Blair247cab72017-07-20 16:52:36 -07001408 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001409 commit_message = '%s-1' % change.subject
1410 self.log.debug("Checking if build %s has changes; commit_message "
1411 "%s; repo_messages %s" % (self, commit_message,
1412 repo_messages))
1413 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001414 self.log.debug(" messages do not match")
1415 return False
1416 self.log.debug(" OK")
1417 return True
1418
James E. Blaird8af5422017-05-24 13:59:40 -07001419 def getWorkspaceRepos(self, projects):
1420 """Return workspace git repo objects for the listed projects
1421
1422 :arg list projects: A list of strings, each the canonical name
1423 of a project.
1424
1425 :returns: A dictionary of {name: repo} for every listed
1426 project.
1427 :rtype: dict
1428
1429 """
1430
1431 repos = {}
1432 for project in projects:
1433 path = os.path.join(self.jobdir.src_root, project)
1434 repo = git.Repo(path)
1435 repos[project] = repo
1436 return repos
1437
Clark Boylanb640e052014-04-03 16:41:46 -07001438
James E. Blair107bb252017-10-13 15:53:16 -07001439class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1440 def doMergeChanges(self, merger, items, repo_state):
1441 # Get a merger in order to update the repos involved in this job.
1442 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1443 merger, items, repo_state)
1444 if not commit: # merge conflict
1445 self.recordResult('MERGER_FAILURE')
1446 return commit
1447
1448 def recordResult(self, result):
1449 build = self.executor_server.job_builds[self.job.unique]
1450 self.executor_server.lock.acquire()
1451 self.executor_server.build_history.append(
1452 BuildHistory(name=build.name, result=result, changes=build.changes,
1453 node=build.node, uuid=build.unique,
1454 ref=build.parameters['zuul']['ref'],
1455 parameters=build.parameters, jobdir=build.jobdir,
1456 pipeline=build.parameters['zuul']['pipeline'])
1457 )
1458 self.executor_server.running_builds.remove(build)
1459 del self.executor_server.job_builds[self.job.unique]
1460 self.executor_server.lock.release()
1461
1462 def runPlaybooks(self, args):
1463 build = self.executor_server.job_builds[self.job.unique]
1464 build.jobdir = self.jobdir
1465
1466 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1467 self.recordResult(result)
1468 return result
1469
James E. Blaira86aaf12017-10-15 20:59:50 -07001470 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001471 build = self.executor_server.job_builds[self.job.unique]
1472
1473 if self.executor_server._run_ansible:
1474 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001475 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001476 else:
1477 if playbook.path:
1478 result = build.run()
1479 else:
1480 result = (self.RESULT_NORMAL, 0)
1481 return result
1482
1483 def getHostList(self, args):
1484 self.log.debug("hostlist")
1485 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1486 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001487 if not host['host_vars'].get('ansible_connection'):
1488 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001489
1490 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001491 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001492 host_vars=dict(ansible_connection='local'),
1493 host_keys=[]))
1494 return hosts
1495
1496
Paul Belanger174a8272017-03-14 13:20:10 -04001497class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1498 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001499
Paul Belanger174a8272017-03-14 13:20:10 -04001500 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001501 they will report that they have started but then pause until
1502 released before reporting completion. This attribute may be
1503 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001504 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001505 be explicitly released.
1506
1507 """
James E. Blairfaf81982017-10-10 15:42:26 -07001508
1509 _job_class = RecordingAnsibleJob
1510
James E. Blairf5dbd002015-12-23 15:26:17 -08001511 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001512 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001513 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001514 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001515 self.hold_jobs_in_build = False
1516 self.lock = threading.Lock()
1517 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001518 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001519 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001520 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001521
James E. Blaira5dba232016-08-08 15:53:24 -07001522 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001523 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001524
1525 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001526 :arg Change change: The :py:class:`~tests.base.FakeChange`
1527 instance which should cause the job to fail. This job
1528 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001529
1530 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001531 l = self.fail_tests.get(name, [])
1532 l.append(change)
1533 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001534
James E. Blair962220f2016-08-03 11:22:38 -07001535 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001536 """Release a held build.
1537
1538 :arg str regex: A regular expression which, if supplied, will
1539 cause only builds with matching names to be released. If
1540 not supplied, all builds will be released.
1541
1542 """
James E. Blair962220f2016-08-03 11:22:38 -07001543 builds = self.running_builds[:]
1544 self.log.debug("Releasing build %s (%s)" % (regex,
1545 len(self.running_builds)))
1546 for build in builds:
1547 if not regex or re.match(regex, build.name):
1548 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001549 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001550 build.release()
1551 else:
1552 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001553 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001554 self.log.debug("Done releasing builds %s (%s)" %
1555 (regex, len(self.running_builds)))
1556
Paul Belanger174a8272017-03-14 13:20:10 -04001557 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001558 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001559 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001560 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001561 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001562 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001563 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001564 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001565 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001566
1567 def stopJob(self, job):
1568 self.log.debug("handle stop")
1569 parameters = json.loads(job.arguments)
1570 uuid = parameters['uuid']
1571 for build in self.running_builds:
1572 if build.unique == uuid:
1573 build.aborted = True
1574 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001575 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001576
James E. Blaira002b032017-04-18 10:35:48 -07001577 def stop(self):
1578 for build in self.running_builds:
1579 build.release()
1580 super(RecordingExecutorServer, self).stop()
1581
Joshua Hesketh50c21782016-10-13 21:34:14 +11001582
Clark Boylanb640e052014-04-03 16:41:46 -07001583class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001584 """A Gearman server for use in tests.
1585
1586 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1587 added to the queue but will not be distributed to workers
1588 until released. This attribute may be changed at any time and
1589 will take effect for subsequently enqueued jobs, but
1590 previously held jobs will still need to be explicitly
1591 released.
1592
1593 """
1594
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001595 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001596 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001597 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001598 if use_ssl:
1599 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1600 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1601 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1602 else:
1603 ssl_ca = None
1604 ssl_cert = None
1605 ssl_key = None
1606
1607 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1608 ssl_cert=ssl_cert,
1609 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001610
1611 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001612 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1613 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001614 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001615 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001616 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001617 elif job.name.startswith(b'merger:'):
1618 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001619 else:
1620 job.waiting = False
1621 if job.waiting:
1622 continue
1623 if job.name in connection.functions:
1624 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001625 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001626 connection.related_jobs[job.handle] = job
1627 job.worker_connection = connection
1628 job.running = True
1629 return job
1630 return None
1631
1632 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001633 """Release a held job.
1634
1635 :arg str regex: A regular expression which, if supplied, will
1636 cause only jobs with matching names to be released. If
1637 not supplied, all jobs will be released.
1638 """
Clark Boylanb640e052014-04-03 16:41:46 -07001639 released = False
1640 qlen = (len(self.high_queue) + len(self.normal_queue) +
1641 len(self.low_queue))
1642 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1643 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001644 match = False
1645 if job.name == b'executor:execute':
1646 parameters = json.loads(job.arguments.decode('utf8'))
1647 if not regex or re.match(regex, parameters.get('job')):
1648 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001649 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001650 if not regex:
1651 match = True
1652 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001653 self.log.debug("releasing queued job %s" %
1654 job.unique)
1655 job.waiting = False
1656 released = True
1657 else:
1658 self.log.debug("not releasing queued job %s" %
1659 job.unique)
1660 if released:
1661 self.wakeConnections()
1662 qlen = (len(self.high_queue) + len(self.normal_queue) +
1663 len(self.low_queue))
1664 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1665
1666
1667class FakeSMTP(object):
1668 log = logging.getLogger('zuul.FakeSMTP')
1669
1670 def __init__(self, messages, server, port):
1671 self.server = server
1672 self.port = port
1673 self.messages = messages
1674
1675 def sendmail(self, from_email, to_email, msg):
1676 self.log.info("Sending email from %s, to %s, with msg %s" % (
1677 from_email, to_email, msg))
1678
1679 headers = msg.split('\n\n', 1)[0]
1680 body = msg.split('\n\n', 1)[1]
1681
1682 self.messages.append(dict(
1683 from_email=from_email,
1684 to_email=to_email,
1685 msg=msg,
1686 headers=headers,
1687 body=body,
1688 ))
1689
1690 return True
1691
1692 def quit(self):
1693 return True
1694
1695
James E. Blairdce6cea2016-12-20 16:45:32 -08001696class FakeNodepool(object):
1697 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001698 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001699
1700 log = logging.getLogger("zuul.test.FakeNodepool")
1701
1702 def __init__(self, host, port, chroot):
1703 self.client = kazoo.client.KazooClient(
1704 hosts='%s:%s%s' % (host, port, chroot))
1705 self.client.start()
1706 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001707 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001708 self.thread = threading.Thread(target=self.run)
1709 self.thread.daemon = True
1710 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001711 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001712
1713 def stop(self):
1714 self._running = False
1715 self.thread.join()
1716 self.client.stop()
1717 self.client.close()
1718
1719 def run(self):
1720 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001721 try:
1722 self._run()
1723 except Exception:
1724 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001725 time.sleep(0.1)
1726
1727 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001728 if self.paused:
1729 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001730 for req in self.getNodeRequests():
1731 self.fulfillRequest(req)
1732
1733 def getNodeRequests(self):
1734 try:
1735 reqids = self.client.get_children(self.REQUEST_ROOT)
1736 except kazoo.exceptions.NoNodeError:
1737 return []
1738 reqs = []
1739 for oid in sorted(reqids):
1740 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001741 try:
1742 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001743 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001744 data['_oid'] = oid
1745 reqs.append(data)
1746 except kazoo.exceptions.NoNodeError:
1747 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001748 return reqs
1749
James E. Blaire18d4602017-01-05 11:17:28 -08001750 def getNodes(self):
1751 try:
1752 nodeids = self.client.get_children(self.NODE_ROOT)
1753 except kazoo.exceptions.NoNodeError:
1754 return []
1755 nodes = []
1756 for oid in sorted(nodeids):
1757 path = self.NODE_ROOT + '/' + oid
1758 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001759 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001760 data['_oid'] = oid
1761 try:
1762 lockfiles = self.client.get_children(path + '/lock')
1763 except kazoo.exceptions.NoNodeError:
1764 lockfiles = []
1765 if lockfiles:
1766 data['_lock'] = True
1767 else:
1768 data['_lock'] = False
1769 nodes.append(data)
1770 return nodes
1771
James E. Blaira38c28e2017-01-04 10:33:20 -08001772 def makeNode(self, request_id, node_type):
1773 now = time.time()
1774 path = '/nodepool/nodes/'
1775 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001776 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001777 provider='test-provider',
1778 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001779 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001780 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001781 public_ipv4='127.0.0.1',
1782 private_ipv4=None,
1783 public_ipv6=None,
1784 allocated_to=request_id,
1785 state='ready',
1786 state_time=now,
1787 created_time=now,
1788 updated_time=now,
1789 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001790 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001791 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001792 if 'fakeuser' in node_type:
1793 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001794 if 'windows' in node_type:
1795 data['connection_type'] = 'winrm'
1796
Clint Byrumf322fe22017-05-10 20:53:12 -07001797 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001798 path = self.client.create(path, data,
1799 makepath=True,
1800 sequence=True)
1801 nodeid = path.split("/")[-1]
1802 return nodeid
1803
James E. Blair6ab79e02017-01-06 10:10:17 -08001804 def addFailRequest(self, request):
1805 self.fail_requests.add(request['_oid'])
1806
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001808 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001809 return
1810 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001811 oid = request['_oid']
1812 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001813
James E. Blair6ab79e02017-01-06 10:10:17 -08001814 if oid in self.fail_requests:
1815 request['state'] = 'failed'
1816 else:
1817 request['state'] = 'fulfilled'
1818 nodes = []
1819 for node in request['node_types']:
1820 nodeid = self.makeNode(oid, node)
1821 nodes.append(nodeid)
1822 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001823
James E. Blaira38c28e2017-01-04 10:33:20 -08001824 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001825 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001826 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001827 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001828 try:
1829 self.client.set(path, data)
1830 except kazoo.exceptions.NoNodeError:
1831 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001832
1833
James E. Blair498059b2016-12-20 13:50:13 -08001834class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001835 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001836 super(ChrootedKazooFixture, self).__init__()
1837
1838 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1839 if ':' in zk_host:
1840 host, port = zk_host.split(':')
1841 else:
1842 host = zk_host
1843 port = None
1844
1845 self.zookeeper_host = host
1846
1847 if not port:
1848 self.zookeeper_port = 2181
1849 else:
1850 self.zookeeper_port = int(port)
1851
Clark Boylan621ec9a2017-04-07 17:41:33 -07001852 self.test_id = test_id
1853
James E. Blair498059b2016-12-20 13:50:13 -08001854 def _setUp(self):
1855 # Make sure the test chroot paths do not conflict
1856 random_bits = ''.join(random.choice(string.ascii_lowercase +
1857 string.ascii_uppercase)
1858 for x in range(8))
1859
Clark Boylan621ec9a2017-04-07 17:41:33 -07001860 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001861 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1862
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001863 self.addCleanup(self._cleanup)
1864
James E. Blair498059b2016-12-20 13:50:13 -08001865 # Ensure the chroot path exists and clean up any pre-existing znodes.
1866 _tmp_client = kazoo.client.KazooClient(
1867 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1868 _tmp_client.start()
1869
1870 if _tmp_client.exists(self.zookeeper_chroot):
1871 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1872
1873 _tmp_client.ensure_path(self.zookeeper_chroot)
1874 _tmp_client.stop()
1875 _tmp_client.close()
1876
James E. Blair498059b2016-12-20 13:50:13 -08001877 def _cleanup(self):
1878 '''Remove the chroot path.'''
1879 # Need a non-chroot'ed client to remove the chroot path
1880 _tmp_client = kazoo.client.KazooClient(
1881 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1882 _tmp_client.start()
1883 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1884 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001885 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001886
1887
Joshua Heskethd78b4482015-09-14 16:56:34 -06001888class MySQLSchemaFixture(fixtures.Fixture):
1889 def setUp(self):
1890 super(MySQLSchemaFixture, self).setUp()
1891
1892 random_bits = ''.join(random.choice(string.ascii_lowercase +
1893 string.ascii_uppercase)
1894 for x in range(8))
1895 self.name = '%s_%s' % (random_bits, os.getpid())
1896 self.passwd = uuid.uuid4().hex
1897 db = pymysql.connect(host="localhost",
1898 user="openstack_citest",
1899 passwd="openstack_citest",
1900 db="openstack_citest")
1901 cur = db.cursor()
1902 cur.execute("create database %s" % self.name)
1903 cur.execute(
1904 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1905 (self.name, self.name, self.passwd))
1906 cur.execute("flush privileges")
1907
1908 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1909 self.passwd,
1910 self.name)
1911 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1912 self.addCleanup(self.cleanup)
1913
1914 def cleanup(self):
1915 db = pymysql.connect(host="localhost",
1916 user="openstack_citest",
1917 passwd="openstack_citest",
1918 db="openstack_citest")
1919 cur = db.cursor()
1920 cur.execute("drop database %s" % self.name)
1921 cur.execute("drop user '%s'@'localhost'" % self.name)
1922 cur.execute("flush privileges")
1923
1924
Maru Newby3fe5f852015-01-13 04:22:14 +00001925class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001926 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001927 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001928
James E. Blair1c236df2017-02-01 14:07:24 -08001929 def attachLogs(self, *args):
1930 def reader():
1931 self._log_stream.seek(0)
1932 while True:
1933 x = self._log_stream.read(4096)
1934 if not x:
1935 break
1936 yield x.encode('utf8')
1937 content = testtools.content.content_from_reader(
1938 reader,
1939 testtools.content_type.UTF8_TEXT,
1940 False)
1941 self.addDetail('logging', content)
1942
Clark Boylanb640e052014-04-03 16:41:46 -07001943 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001944 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001945 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1946 try:
1947 test_timeout = int(test_timeout)
1948 except ValueError:
1949 # If timeout value is invalid do not set a timeout.
1950 test_timeout = 0
1951 if test_timeout > 0:
1952 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1953
1954 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1955 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1956 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1957 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1958 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1959 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1960 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1961 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1962 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1963 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001964 self._log_stream = StringIO()
1965 self.addOnException(self.attachLogs)
1966 else:
1967 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001968
James E. Blair73b41772017-05-22 13:22:55 -07001969 # NOTE(jeblair): this is temporary extra debugging to try to
1970 # track down a possible leak.
1971 orig_git_repo_init = git.Repo.__init__
1972
1973 def git_repo_init(myself, *args, **kw):
1974 orig_git_repo_init(myself, *args, **kw)
1975 self.log.debug("Created git repo 0x%x %s" %
1976 (id(myself), repr(myself)))
1977
1978 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1979 git_repo_init))
1980
James E. Blair1c236df2017-02-01 14:07:24 -08001981 handler = logging.StreamHandler(self._log_stream)
1982 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1983 '%(levelname)-8s %(message)s')
1984 handler.setFormatter(formatter)
1985
1986 logger = logging.getLogger()
1987 logger.setLevel(logging.DEBUG)
1988 logger.addHandler(handler)
1989
Clark Boylan3410d532017-04-25 12:35:29 -07001990 # Make sure we don't carry old handlers around in process state
1991 # which slows down test runs
1992 self.addCleanup(logger.removeHandler, handler)
1993 self.addCleanup(handler.close)
1994 self.addCleanup(handler.flush)
1995
James E. Blair1c236df2017-02-01 14:07:24 -08001996 # NOTE(notmorgan): Extract logging overrides for specific
1997 # libraries from the OS_LOG_DEFAULTS env and create loggers
1998 # for each. This is used to limit the output during test runs
1999 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08002000 log_defaults_from_env = os.environ.get(
2001 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05002002 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07002003
James E. Blairdce6cea2016-12-20 16:45:32 -08002004 if log_defaults_from_env:
2005 for default in log_defaults_from_env.split(','):
2006 try:
2007 name, level_str = default.split('=', 1)
2008 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08002009 logger = logging.getLogger(name)
2010 logger.setLevel(level)
2011 logger.addHandler(handler)
2012 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08002013 except ValueError:
2014 # NOTE(notmorgan): Invalid format of the log default,
2015 # skip and don't try and apply a logger for the
2016 # specified module
2017 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07002018
Maru Newby3fe5f852015-01-13 04:22:14 +00002019
2020class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07002021 """A test case with a functioning Zuul.
2022
2023 The following class variables are used during test setup and can
2024 be overidden by subclasses but are effectively read-only once a
2025 test method starts running:
2026
2027 :cvar str config_file: This points to the main zuul config file
2028 within the fixtures directory. Subclasses may override this
2029 to obtain a different behavior.
2030
2031 :cvar str tenant_config_file: This is the tenant config file
2032 (which specifies from what git repos the configuration should
2033 be loaded). It defaults to the value specified in
2034 `config_file` but can be overidden by subclasses to obtain a
2035 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07002036 configuration. See also the :py:func:`simple_layout`
2037 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07002038
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002039 :cvar bool create_project_keys: Indicates whether Zuul should
2040 auto-generate keys for each project, or whether the test
2041 infrastructure should insert dummy keys to save time during
2042 startup. Defaults to False.
2043
James E. Blaire7b99a02016-08-05 14:27:34 -07002044 The following are instance variables that are useful within test
2045 methods:
2046
2047 :ivar FakeGerritConnection fake_<connection>:
2048 A :py:class:`~tests.base.FakeGerritConnection` will be
2049 instantiated for each connection present in the config file
2050 and stored here. For instance, `fake_gerrit` will hold the
2051 FakeGerritConnection object for a connection named `gerrit`.
2052
2053 :ivar FakeGearmanServer gearman_server: An instance of
2054 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
2055 server that all of the Zuul components in this test use to
2056 communicate with each other.
2057
Paul Belanger174a8272017-03-14 13:20:10 -04002058 :ivar RecordingExecutorServer executor_server: An instance of
2059 :py:class:`~tests.base.RecordingExecutorServer` which is the
2060 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07002061
2062 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
2063 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04002064 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07002065 list upon completion.
2066
2067 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
2068 objects representing completed builds. They are appended to
2069 the list in the order they complete.
2070
2071 """
2072
James E. Blair83005782015-12-11 14:46:03 -08002073 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07002074 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002075 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002076 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07002077
2078 def _startMerger(self):
2079 self.merge_server = zuul.merger.server.MergeServer(self.config,
2080 self.connections)
2081 self.merge_server.start()
2082
Maru Newby3fe5f852015-01-13 04:22:14 +00002083 def setUp(self):
2084 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08002085
2086 self.setupZK()
2087
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002088 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07002089 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002090 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2091 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002092 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002093 tmp_root = tempfile.mkdtemp(
2094 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002095 self.test_root = os.path.join(tmp_root, "zuul-test")
2096 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002097 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002098 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002099 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002100 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2101 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002102
2103 if os.path.exists(self.test_root):
2104 shutil.rmtree(self.test_root)
2105 os.makedirs(self.test_root)
2106 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002107 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002108 os.makedirs(self.merger_state_root)
2109 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002110
2111 # Make per test copy of Configuration.
2112 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002113 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2114 if not os.path.exists(self.private_key_file):
2115 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2116 shutil.copy(src_private_key_file, self.private_key_file)
2117 shutil.copy('{}.pub'.format(src_private_key_file),
2118 '{}.pub'.format(self.private_key_file))
2119 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002120 self.config.set('scheduler', 'tenant_config',
2121 os.path.join(
2122 FIXTURE_DIR,
2123 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002124 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05002125 self.config.set(
2126 'scheduler', 'command_socket',
2127 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05002128 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002129 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002130 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002131 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05002132 self.config.set(
2133 'executor', 'command_socket',
2134 os.path.join(self.test_root, 'executor.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07002135
Clark Boylanb640e052014-04-03 16:41:46 -07002136 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07002137 if self.config.has_section('statsd'):
2138 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07002139 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002140
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002141 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002142
2143 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002144 self.log.info("Gearman server on port %s" %
2145 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002146 if self.use_ssl:
2147 self.log.info('SSL enabled for gearman')
2148 self.config.set(
2149 'gearman', 'ssl_ca',
2150 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2151 self.config.set(
2152 'gearman', 'ssl_cert',
2153 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2154 self.config.set(
2155 'gearman', 'ssl_key',
2156 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002157
James E. Blaire511d2f2016-12-08 15:22:26 -08002158 gerritsource.GerritSource.replication_timeout = 1.5
2159 gerritsource.GerritSource.replication_retry_interval = 0.5
2160 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002161
Joshua Hesketh352264b2015-08-11 23:42:08 +10002162 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002163 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002164
Jan Hruban7083edd2015-08-21 14:00:54 +02002165 self.webapp = zuul.webapp.WebApp(
2166 self.sched, port=0, listen_address='127.0.0.1')
2167
Jan Hruban6b71aff2015-10-22 16:58:08 +02002168 self.event_queues = [
2169 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002170 self.sched.trigger_event_queue,
2171 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002172 ]
2173
James E. Blairfef78942016-03-11 16:28:56 -08002174 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002175 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002176
Paul Belanger174a8272017-03-14 13:20:10 -04002177 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002178 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002179 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002180 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002181 _test_root=self.test_root,
2182 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002183 self.executor_server.start()
2184 self.history = self.executor_server.build_history
2185 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002186
Paul Belanger174a8272017-03-14 13:20:10 -04002187 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002188 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002189 self.merge_client = zuul.merger.client.MergeClient(
2190 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002191 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002192 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002193 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002194
James E. Blair0d5a36e2017-02-21 10:53:44 -05002195 self.fake_nodepool = FakeNodepool(
2196 self.zk_chroot_fixture.zookeeper_host,
2197 self.zk_chroot_fixture.zookeeper_port,
2198 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002199
Paul Belanger174a8272017-03-14 13:20:10 -04002200 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002201 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002202 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002203 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002204
Clark Boylanb640e052014-04-03 16:41:46 -07002205 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002206 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002207 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002208 # Cleanups are run in reverse order
2209 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002210 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002211 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002212
James E. Blairb9c0d772017-03-03 14:34:49 -08002213 self.sched.reconfigure(self.config)
2214 self.sched.resume()
2215
Tobias Henkel7df274b2017-05-26 17:41:11 +02002216 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002217 # Set up gerrit related fakes
2218 # Set a changes database so multiple FakeGerrit's can report back to
2219 # a virtual canonical database given by the configured hostname
2220 self.gerrit_changes_dbs = {}
2221
2222 def getGerritConnection(driver, name, config):
2223 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2224 con = FakeGerritConnection(driver, name, config,
2225 changes_db=db,
2226 upstream_root=self.upstream_root)
2227 self.event_queues.append(con.event_queue)
2228 setattr(self, 'fake_' + name, con)
2229 return con
2230
2231 self.useFixture(fixtures.MonkeyPatch(
2232 'zuul.driver.gerrit.GerritDriver.getConnection',
2233 getGerritConnection))
2234
Gregory Haynes4fc12542015-04-22 20:38:06 -07002235 def getGithubConnection(driver, name, config):
2236 con = FakeGithubConnection(driver, name, config,
2237 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002238 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002239 setattr(self, 'fake_' + name, con)
2240 return con
2241
2242 self.useFixture(fixtures.MonkeyPatch(
2243 'zuul.driver.github.GithubDriver.getConnection',
2244 getGithubConnection))
2245
James E. Blaire511d2f2016-12-08 15:22:26 -08002246 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002247 # TODO(jhesketh): This should come from lib.connections for better
2248 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002249 # Register connections from the config
2250 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002251
Joshua Hesketh352264b2015-08-11 23:42:08 +10002252 def FakeSMTPFactory(*args, **kw):
2253 args = [self.smtp_messages] + list(args)
2254 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002255
Joshua Hesketh352264b2015-08-11 23:42:08 +10002256 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002257
James E. Blaire511d2f2016-12-08 15:22:26 -08002258 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002259 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002260 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002261
James E. Blair83005782015-12-11 14:46:03 -08002262 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002263 # This creates the per-test configuration object. It can be
2264 # overriden by subclasses, but should not need to be since it
2265 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002266 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002267 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002268
James E. Blair39840362017-06-23 20:34:02 +01002269 sections = ['zuul', 'scheduler', 'executor', 'merger']
2270 for section in sections:
2271 if not self.config.has_section(section):
2272 self.config.add_section(section)
2273
James E. Blair06cc3922017-04-19 10:08:10 -07002274 if not self.setupSimpleLayout():
2275 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002276 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002277 self.tenant_config_file)
2278 git_path = os.path.join(
2279 os.path.dirname(
2280 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2281 'git')
2282 if os.path.exists(git_path):
2283 for reponame in os.listdir(git_path):
2284 project = reponame.replace('_', '/')
2285 self.copyDirToRepo(project,
2286 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002287 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002288 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002289 self.setupAllProjectKeys()
2290
James E. Blair06cc3922017-04-19 10:08:10 -07002291 def setupSimpleLayout(self):
2292 # If the test method has been decorated with a simple_layout,
2293 # use that instead of the class tenant_config_file. Set up a
2294 # single config-project with the specified layout, and
2295 # initialize repos for all of the 'project' entries which
2296 # appear in the layout.
2297 test_name = self.id().split('.')[-1]
2298 test = getattr(self, test_name)
2299 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002300 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002301 else:
2302 return False
2303
James E. Blairb70e55a2017-04-19 12:57:02 -07002304 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002305 path = os.path.join(FIXTURE_DIR, path)
2306 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002307 data = f.read()
2308 layout = yaml.safe_load(data)
2309 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002310 untrusted_projects = []
2311 for item in layout:
2312 if 'project' in item:
2313 name = item['project']['name']
2314 untrusted_projects.append(name)
2315 self.init_repo(name)
2316 self.addCommitToRepo(name, 'initial commit',
2317 files={'README': ''},
2318 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002319 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002320 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002321 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002322 for fn in zuul.configloader.as_list(
2323 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002324 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002325 for fn in zuul.configloader.as_list(
2326 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002327 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002328
2329 root = os.path.join(self.test_root, "config")
2330 if not os.path.exists(root):
2331 os.makedirs(root)
2332 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2333 config = [{'tenant':
2334 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002335 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002336 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002337 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002338 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002339 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002340 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002341 os.path.join(FIXTURE_DIR, f.name))
2342
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002343 self.init_repo('org/common-config')
2344 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002345 files, branch='master', tag='init')
2346
2347 return True
2348
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002349 def setupAllProjectKeys(self):
2350 if self.create_project_keys:
2351 return
2352
James E. Blair39840362017-06-23 20:34:02 +01002353 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002354 with open(os.path.join(FIXTURE_DIR, path)) as f:
2355 tenant_config = yaml.safe_load(f.read())
2356 for tenant in tenant_config:
2357 sources = tenant['tenant']['source']
2358 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002359 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002360 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002361 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002362 self.setupProjectKeys(source, project)
2363
2364 def setupProjectKeys(self, source, project):
2365 # Make sure we set up an RSA key for the project so that we
2366 # don't spend time generating one:
2367
James E. Blair6459db12017-06-29 14:57:20 -07002368 if isinstance(project, dict):
2369 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002370 key_root = os.path.join(self.state_root, 'keys')
2371 if not os.path.isdir(key_root):
2372 os.mkdir(key_root, 0o700)
2373 private_key_file = os.path.join(key_root, source, project + '.pem')
2374 private_key_dir = os.path.dirname(private_key_file)
2375 self.log.debug("Installing test keys for project %s at %s" % (
2376 project, private_key_file))
2377 if not os.path.isdir(private_key_dir):
2378 os.makedirs(private_key_dir)
2379 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2380 with open(private_key_file, 'w') as o:
2381 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002382
James E. Blair498059b2016-12-20 13:50:13 -08002383 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002384 self.zk_chroot_fixture = self.useFixture(
2385 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002386 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002387 self.zk_chroot_fixture.zookeeper_host,
2388 self.zk_chroot_fixture.zookeeper_port,
2389 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002390
James E. Blair96c6bf82016-01-15 16:20:40 -08002391 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002392 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002393
2394 files = {}
2395 for (dirpath, dirnames, filenames) in os.walk(source_path):
2396 for filename in filenames:
2397 test_tree_filepath = os.path.join(dirpath, filename)
2398 common_path = os.path.commonprefix([test_tree_filepath,
2399 source_path])
2400 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2401 with open(test_tree_filepath, 'r') as f:
2402 content = f.read()
2403 files[relative_filepath] = content
2404 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002405 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002406
James E. Blaire18d4602017-01-05 11:17:28 -08002407 def assertNodepoolState(self):
2408 # Make sure that there are no pending requests
2409
2410 requests = self.fake_nodepool.getNodeRequests()
2411 self.assertEqual(len(requests), 0)
2412
2413 nodes = self.fake_nodepool.getNodes()
2414 for node in nodes:
2415 self.assertFalse(node['_lock'], "Node %s is locked" %
2416 (node['_oid'],))
2417
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002418 def assertNoGeneratedKeys(self):
2419 # Make sure that Zuul did not generate any project keys
2420 # (unless it was supposed to).
2421
2422 if self.create_project_keys:
2423 return
2424
2425 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2426 test_key = i.read()
2427
2428 key_root = os.path.join(self.state_root, 'keys')
2429 for root, dirname, files in os.walk(key_root):
2430 for fn in files:
2431 with open(os.path.join(root, fn)) as f:
2432 self.assertEqual(test_key, f.read())
2433
Clark Boylanb640e052014-04-03 16:41:46 -07002434 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002435 self.log.debug("Assert final state")
2436 # Make sure no jobs are running
2437 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002438 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002439 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002440 gc.collect()
2441 for obj in gc.get_objects():
2442 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002443 self.log.debug("Leaked git repo object: 0x%x %s" %
2444 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002445 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002446 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002447 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002448 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002449 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002450 for tenant in self.sched.abide.tenants.values():
2451 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002452 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002453 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002454
2455 def shutdown(self):
2456 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002457 self.executor_server.hold_jobs_in_build = False
2458 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002459 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002460 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002461 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002462 self.sched.stop()
2463 self.sched.join()
2464 self.statsd.stop()
2465 self.statsd.join()
2466 self.webapp.stop()
2467 self.webapp.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002468 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002469 self.fake_nodepool.stop()
2470 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002471 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002472 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002473 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002474 # Further the pydevd threads also need to be whitelisted so debugging
2475 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002476 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002477 'pydevd.CommandThread',
2478 'pydevd.Reader',
2479 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002480 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002481 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002482 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002483 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002484 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002485 log_str = ""
2486 for thread_id, stack_frame in sys._current_frames().items():
2487 log_str += "Thread: %s\n" % thread_id
2488 log_str += "".join(traceback.format_stack(stack_frame))
2489 self.log.debug(log_str)
2490 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002491
James E. Blaira002b032017-04-18 10:35:48 -07002492 def assertCleanShutdown(self):
2493 pass
2494
James E. Blairc4ba97a2017-04-19 16:26:24 -07002495 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002496 parts = project.split('/')
2497 path = os.path.join(self.upstream_root, *parts[:-1])
2498 if not os.path.exists(path):
2499 os.makedirs(path)
2500 path = os.path.join(self.upstream_root, project)
2501 repo = git.Repo.init(path)
2502
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002503 with repo.config_writer() as config_writer:
2504 config_writer.set_value('user', 'email', 'user@example.com')
2505 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002506
Clark Boylanb640e052014-04-03 16:41:46 -07002507 repo.index.commit('initial commit')
2508 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002509 if tag:
2510 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002511
James E. Blair97d902e2014-08-21 13:25:56 -07002512 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002513 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002514 repo.git.clean('-x', '-f', '-d')
2515
James E. Blair97d902e2014-08-21 13:25:56 -07002516 def create_branch(self, project, branch):
2517 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002518 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002519 fn = os.path.join(path, 'README')
2520
2521 branch_head = repo.create_head(branch)
2522 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002523 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002524 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002525 f.close()
2526 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002527 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002528
James E. Blair97d902e2014-08-21 13:25:56 -07002529 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002530 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002531 repo.git.clean('-x', '-f', '-d')
2532
Sachi King9f16d522016-03-16 12:20:45 +11002533 def create_commit(self, project):
2534 path = os.path.join(self.upstream_root, project)
2535 repo = git.Repo(path)
2536 repo.head.reference = repo.heads['master']
2537 file_name = os.path.join(path, 'README')
2538 with open(file_name, 'a') as f:
2539 f.write('creating fake commit\n')
2540 repo.index.add([file_name])
2541 commit = repo.index.commit('Creating a fake commit')
2542 return commit.hexsha
2543
James E. Blairf4a5f022017-04-18 14:01:10 -07002544 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002545 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002546 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002547 while len(self.builds):
2548 self.release(self.builds[0])
2549 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002550 i += 1
2551 if count is not None and i >= count:
2552 break
James E. Blairb8c16472015-05-05 14:55:26 -07002553
James E. Blairdf25ddc2017-07-08 07:57:09 -07002554 def getSortedBuilds(self):
2555 "Return the list of currently running builds sorted by name"
2556
2557 return sorted(self.builds, key=lambda x: x.name)
2558
Clark Boylanb640e052014-04-03 16:41:46 -07002559 def release(self, job):
2560 if isinstance(job, FakeBuild):
2561 job.release()
2562 else:
2563 job.waiting = False
2564 self.log.debug("Queued job %s released" % job.unique)
2565 self.gearman_server.wakeConnections()
2566
2567 def getParameter(self, job, name):
2568 if isinstance(job, FakeBuild):
2569 return job.parameters[name]
2570 else:
2571 parameters = json.loads(job.arguments)
2572 return parameters[name]
2573
Clark Boylanb640e052014-04-03 16:41:46 -07002574 def haveAllBuildsReported(self):
2575 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002576 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002577 return False
2578 # Find out if every build that the worker has completed has been
2579 # reported back to Zuul. If it hasn't then that means a Gearman
2580 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002581 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002582 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002583 if not zbuild:
2584 # It has already been reported
2585 continue
2586 # It hasn't been reported yet.
2587 return False
2588 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002589 worker = self.executor_server.executor_worker
2590 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002591 if connection.state == 'GRAB_WAIT':
2592 return False
2593 return True
2594
2595 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002596 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002597 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002598 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002599 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002600 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002601 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002602 for j in conn.related_jobs.values():
2603 if j.unique == build.uuid:
2604 client_job = j
2605 break
2606 if not client_job:
2607 self.log.debug("%s is not known to the gearman client" %
2608 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002609 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002610 if not client_job.handle:
2611 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002612 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002613 server_job = self.gearman_server.jobs.get(client_job.handle)
2614 if not server_job:
2615 self.log.debug("%s is not known to the gearman server" %
2616 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002617 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002618 if not hasattr(server_job, 'waiting'):
2619 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002620 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002621 if server_job.waiting:
2622 continue
James E. Blair17302972016-08-10 16:11:42 -07002623 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002624 self.log.debug("%s has not reported start" % build)
2625 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002626 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002627 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002628 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002629 if worker_build:
2630 if worker_build.isWaiting():
2631 continue
2632 else:
2633 self.log.debug("%s is running" % worker_build)
2634 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002635 else:
James E. Blair962220f2016-08-03 11:22:38 -07002636 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002637 return False
James E. Blaira002b032017-04-18 10:35:48 -07002638 for (build_uuid, job_worker) in \
2639 self.executor_server.job_workers.items():
2640 if build_uuid not in seen_builds:
2641 self.log.debug("%s is not finalized" % build_uuid)
2642 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002643 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002644
James E. Blairdce6cea2016-12-20 16:45:32 -08002645 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002646 if self.fake_nodepool.paused:
2647 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002648 if self.sched.nodepool.requests:
2649 return False
2650 return True
2651
James E. Blaira615c362017-10-02 17:34:42 -07002652 def areAllMergeJobsWaiting(self):
2653 for client_job in list(self.merge_client.jobs):
2654 if not client_job.handle:
2655 self.log.debug("%s has no handle" % client_job)
2656 return False
2657 server_job = self.gearman_server.jobs.get(client_job.handle)
2658 if not server_job:
2659 self.log.debug("%s is not known to the gearman server" %
2660 client_job)
2661 return False
2662 if not hasattr(server_job, 'waiting'):
2663 self.log.debug("%s is being enqueued" % server_job)
2664 return False
2665 if server_job.waiting:
2666 self.log.debug("%s is waiting" % server_job)
2667 continue
2668 self.log.debug("%s is not waiting" % server_job)
2669 return False
2670 return True
2671
Jan Hruban6b71aff2015-10-22 16:58:08 +02002672 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002673 for event_queue in self.event_queues:
2674 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002675
2676 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002677 for event_queue in self.event_queues:
2678 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002679
Clark Boylanb640e052014-04-03 16:41:46 -07002680 def waitUntilSettled(self):
2681 self.log.debug("Waiting until settled...")
2682 start = time.time()
2683 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002684 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002685 self.log.error("Timeout waiting for Zuul to settle")
2686 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002687 for event_queue in self.event_queues:
2688 self.log.error(" %s: %s" %
2689 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002690 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002691 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002692 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002693 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002694 self.log.error("All requests completed: %s" %
2695 (self.areAllNodeRequestsComplete(),))
2696 self.log.error("Merge client jobs: %s" %
2697 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002698 raise Exception("Timeout waiting for Zuul to settle")
2699 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002700
Paul Belanger174a8272017-03-14 13:20:10 -04002701 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002702 # have all build states propogated to zuul?
2703 if self.haveAllBuildsReported():
2704 # Join ensures that the queue is empty _and_ events have been
2705 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002706 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002707 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002708 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002709 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002710 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002711 self.areAllNodeRequestsComplete() and
2712 all(self.eventQueuesEmpty())):
2713 # The queue empty check is placed at the end to
2714 # ensure that if a component adds an event between
2715 # when locked the run handler and checked that the
2716 # components were stable, we don't erroneously
2717 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002718 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002719 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002720 self.log.debug("...settled.")
2721 return
2722 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002723 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002724 self.sched.wake_event.wait(0.1)
2725
2726 def countJobResults(self, jobs, result):
2727 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002728 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002729
Monty Taylor0d926122017-05-24 08:07:56 -05002730 def getBuildByName(self, name):
2731 for build in self.builds:
2732 if build.name == name:
2733 return build
2734 raise Exception("Unable to find build %s" % name)
2735
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002736 def assertJobNotInHistory(self, name, project=None):
2737 for job in self.history:
2738 if (project is None or
2739 job.parameters['zuul']['project']['name'] == project):
2740 self.assertNotEqual(job.name, name,
2741 'Job %s found in history' % name)
2742
James E. Blair96c6bf82016-01-15 16:20:40 -08002743 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002744 for job in self.history:
2745 if (job.name == name and
2746 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002747 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002748 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002749 raise Exception("Unable to find job %s in history" % name)
2750
2751 def assertEmptyQueues(self):
2752 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002753 for tenant in self.sched.abide.tenants.values():
2754 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002755 for pipeline_queue in pipeline.queues:
2756 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002757 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002758 pipeline.name, pipeline_queue.name,
2759 pipeline_queue.queue))
2760 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002761 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002762
2763 def assertReportedStat(self, key, value=None, kind=None):
2764 start = time.time()
2765 while time.time() < (start + 5):
2766 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002767 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002768 if key == k:
2769 if value is None and kind is None:
2770 return
2771 elif value:
2772 if value == v:
2773 return
2774 elif kind:
2775 if v.endswith('|' + kind):
2776 return
2777 time.sleep(0.1)
2778
Clark Boylanb640e052014-04-03 16:41:46 -07002779 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002780
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002781 def assertBuilds(self, builds):
2782 """Assert that the running builds are as described.
2783
2784 The list of running builds is examined and must match exactly
2785 the list of builds described by the input.
2786
2787 :arg list builds: A list of dictionaries. Each item in the
2788 list must match the corresponding build in the build
2789 history, and each element of the dictionary must match the
2790 corresponding attribute of the build.
2791
2792 """
James E. Blair3158e282016-08-19 09:34:11 -07002793 try:
2794 self.assertEqual(len(self.builds), len(builds))
2795 for i, d in enumerate(builds):
2796 for k, v in d.items():
2797 self.assertEqual(
2798 getattr(self.builds[i], k), v,
2799 "Element %i in builds does not match" % (i,))
2800 except Exception:
2801 for build in self.builds:
2802 self.log.error("Running build: %s" % build)
2803 else:
2804 self.log.error("No running builds")
2805 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002806
James E. Blairb536ecc2016-08-31 10:11:42 -07002807 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002808 """Assert that the completed builds are as described.
2809
2810 The list of completed builds is examined and must match
2811 exactly the list of builds described by the input.
2812
2813 :arg list history: A list of dictionaries. Each item in the
2814 list must match the corresponding build in the build
2815 history, and each element of the dictionary must match the
2816 corresponding attribute of the build.
2817
James E. Blairb536ecc2016-08-31 10:11:42 -07002818 :arg bool ordered: If true, the history must match the order
2819 supplied, if false, the builds are permitted to have
2820 arrived in any order.
2821
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002822 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002823 def matches(history_item, item):
2824 for k, v in item.items():
2825 if getattr(history_item, k) != v:
2826 return False
2827 return True
James E. Blair3158e282016-08-19 09:34:11 -07002828 try:
2829 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002830 if ordered:
2831 for i, d in enumerate(history):
2832 if not matches(self.history[i], d):
2833 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002834 "Element %i in history does not match %s" %
2835 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002836 else:
2837 unseen = self.history[:]
2838 for i, d in enumerate(history):
2839 found = False
2840 for unseen_item in unseen:
2841 if matches(unseen_item, d):
2842 found = True
2843 unseen.remove(unseen_item)
2844 break
2845 if not found:
2846 raise Exception("No match found for element %i "
2847 "in history" % (i,))
2848 if unseen:
2849 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002850 except Exception:
2851 for build in self.history:
2852 self.log.error("Completed build: %s" % build)
2853 else:
2854 self.log.error("No completed builds")
2855 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002856
James E. Blair6ac368c2016-12-22 18:07:20 -08002857 def printHistory(self):
2858 """Log the build history.
2859
2860 This can be useful during tests to summarize what jobs have
2861 completed.
2862
2863 """
2864 self.log.debug("Build history:")
2865 for build in self.history:
2866 self.log.debug(build)
2867
James E. Blair59fdbac2015-12-07 17:08:06 -08002868 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002869 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2870
James E. Blair9ea70072017-04-19 16:05:30 -07002871 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002872 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002873 if not os.path.exists(root):
2874 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002875 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2876 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002877- tenant:
2878 name: openstack
2879 source:
2880 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002881 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002882 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002883 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002884 - org/project
2885 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002886 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002887 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002888 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002889 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002890 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002891
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002892 def addTagToRepo(self, project, name, sha):
2893 path = os.path.join(self.upstream_root, project)
2894 repo = git.Repo(path)
2895 repo.git.tag(name, sha)
2896
2897 def delTagFromRepo(self, project, name):
2898 path = os.path.join(self.upstream_root, project)
2899 repo = git.Repo(path)
2900 repo.git.tag('-d', name)
2901
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002902 def addCommitToRepo(self, project, message, files,
2903 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002904 path = os.path.join(self.upstream_root, project)
2905 repo = git.Repo(path)
2906 repo.head.reference = branch
2907 zuul.merger.merger.reset_repo_to_head(repo)
2908 for fn, content in files.items():
2909 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002910 try:
2911 os.makedirs(os.path.dirname(fn))
2912 except OSError:
2913 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002914 with open(fn, 'w') as f:
2915 f.write(content)
2916 repo.index.add([fn])
2917 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002918 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002919 repo.heads[branch].commit = commit
2920 repo.head.reference = branch
2921 repo.git.clean('-x', '-f', '-d')
2922 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002923 if tag:
2924 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002925 return before
2926
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002927 def commitConfigUpdate(self, project_name, source_name):
2928 """Commit an update to zuul.yaml
2929
2930 This overwrites the zuul.yaml in the specificed project with
2931 the contents specified.
2932
2933 :arg str project_name: The name of the project containing
2934 zuul.yaml (e.g., common-config)
2935
2936 :arg str source_name: The path to the file (underneath the
2937 test fixture directory) whose contents should be used to
2938 replace zuul.yaml.
2939 """
2940
2941 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002942 files = {}
2943 with open(source_path, 'r') as f:
2944 data = f.read()
2945 layout = yaml.safe_load(data)
2946 files['zuul.yaml'] = data
2947 for item in layout:
2948 if 'job' in item:
2949 jobname = item['job']['name']
2950 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002951 before = self.addCommitToRepo(
2952 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002953 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002954 return before
2955
Clint Byrum627ba362017-08-14 13:20:40 -07002956 def newTenantConfig(self, source_name):
2957 """ Use this to update the tenant config file in tests
2958
2959 This will update self.tenant_config_file to point to a temporary file
2960 for the duration of this particular test. The content of that file will
2961 be taken from FIXTURE_DIR/source_name
2962
2963 After the test the original value of self.tenant_config_file will be
2964 restored.
2965
2966 :arg str source_name: The path of the file under
2967 FIXTURE_DIR that will be used to populate the new tenant
2968 config file.
2969 """
2970 source_path = os.path.join(FIXTURE_DIR, source_name)
2971 orig_tenant_config_file = self.tenant_config_file
2972 with tempfile.NamedTemporaryFile(
2973 delete=False, mode='wb') as new_tenant_config:
2974 self.tenant_config_file = new_tenant_config.name
2975 with open(source_path, mode='rb') as source_tenant_config:
2976 new_tenant_config.write(source_tenant_config.read())
2977 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2978 self.setupAllProjectKeys()
2979 self.log.debug(
2980 'tenant_config_file = {}'.format(self.tenant_config_file))
2981
2982 def _restoreTenantConfig():
2983 self.log.debug(
2984 'restoring tenant_config_file = {}'.format(
2985 orig_tenant_config_file))
2986 os.unlink(self.tenant_config_file)
2987 self.tenant_config_file = orig_tenant_config_file
2988 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2989 self.addCleanup(_restoreTenantConfig)
2990
James E. Blair7fc8daa2016-08-08 15:37:15 -07002991 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002992
James E. Blair7fc8daa2016-08-08 15:37:15 -07002993 """Inject a Fake (Gerrit) event.
2994
2995 This method accepts a JSON-encoded event and simulates Zuul
2996 having received it from Gerrit. It could (and should)
2997 eventually apply to any connection type, but is currently only
2998 used with Gerrit connections. The name of the connection is
2999 used to look up the corresponding server, and the event is
3000 simulated as having been received by all Zuul connections
3001 attached to that server. So if two Gerrit connections in Zuul
3002 are connected to the same Gerrit server, and you invoke this
3003 method specifying the name of one of them, the event will be
3004 received by both.
3005
3006 .. note::
3007
3008 "self.fake_gerrit.addEvent" calls should be migrated to
3009 this method.
3010
3011 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07003012 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07003013 :arg str event: The JSON-encoded event.
3014
3015 """
3016 specified_conn = self.connections.connections[connection]
3017 for conn in self.connections.connections.values():
3018 if (isinstance(conn, specified_conn.__class__) and
3019 specified_conn.server == conn.server):
3020 conn.addEvent(event)
3021
James E. Blaird8af5422017-05-24 13:59:40 -07003022 def getUpstreamRepos(self, projects):
3023 """Return upstream git repo objects for the listed projects
3024
3025 :arg list projects: A list of strings, each the canonical name
3026 of a project.
3027
3028 :returns: A dictionary of {name: repo} for every listed
3029 project.
3030 :rtype: dict
3031
3032 """
3033
3034 repos = {}
3035 for project in projects:
3036 # FIXME(jeblair): the upstream root does not yet have a
3037 # hostname component; that needs to be added, and this
3038 # line removed:
3039 tmp_project_name = '/'.join(project.split('/')[1:])
3040 path = os.path.join(self.upstream_root, tmp_project_name)
3041 repo = git.Repo(path)
3042 repos[project] = repo
3043 return repos
3044
James E. Blair3f876d52016-07-22 13:07:14 -07003045
3046class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04003047 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07003048 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11003049
Jamie Lennox7655b552017-03-17 12:33:38 +11003050 @contextmanager
3051 def jobLog(self, build):
3052 """Print job logs on assertion errors
3053
3054 This method is a context manager which, if it encounters an
3055 ecxeption, adds the build log to the debug output.
3056
3057 :arg Build build: The build that's being asserted.
3058 """
3059 try:
3060 yield
3061 except Exception:
3062 path = os.path.join(self.test_root, build.uuid,
3063 'work', 'logs', 'job-output.txt')
3064 with open(path) as f:
3065 self.log.debug(f.read())
3066 raise
3067
Joshua Heskethd78b4482015-09-14 16:56:34 -06003068
Paul Belanger0a21f0a2017-06-13 13:14:42 -04003069class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04003070 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04003071 use_ssl = True
3072
3073
Joshua Heskethd78b4482015-09-14 16:56:34 -06003074class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08003075 def setup_config(self):
3076 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06003077 for section_name in self.config.sections():
3078 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
3079 section_name, re.I)
3080 if not con_match:
3081 continue
3082
3083 if self.config.get(section_name, 'driver') == 'sql':
3084 f = MySQLSchemaFixture()
3085 self.useFixture(f)
3086 if (self.config.get(section_name, 'dburi') ==
3087 '$MYSQL_FIXTURE_DBURI$'):
3088 self.config.set(section_name, 'dburi', f.dburi)