blob: 7e63129ead895d1a8403f771ff3fc4d7aa731d1e [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
677 def __init__(self):
678 self._repos = {}
679
Tobias Henkel64e37a02017-08-02 10:13:30 +0200680 def user(self, login):
681 return self.FakeUser(login)
682
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200683 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200684 return self._repos.get((owner, proj), None)
685
686 def repo_from_project(self, project):
687 # This is a convenience method for the tests.
688 owner, proj = project.split('/')
689 return self.repository(owner, proj)
690
691 def addProject(self, project):
692 owner, proj = project.name.split('/')
693 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200694
Tobias Henkel64e37a02017-08-02 10:13:30 +0200695
Gregory Haynes4fc12542015-04-22 20:38:06 -0700696class FakeGithubPullRequest(object):
697
698 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800699 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700700 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700701 """Creates a new PR with several commits.
702 Sends an event about opened PR."""
703 self.github = github
704 self.source = github
705 self.number = number
706 self.project = project
707 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100708 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700709 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100710 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700711 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100712 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700713 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100714 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100715 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800716 self.reviews = []
717 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700718 self.updated_at = None
719 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100720 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100721 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700722 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700723 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100724 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700725 self._updateTimeStamp()
726
Jan Hruban570d01c2016-03-10 21:51:32 +0100727 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700728 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100729 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700730 self._updateTimeStamp()
731
Jan Hruban570d01c2016-03-10 21:51:32 +0100732 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700733 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100734 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700735 self._updateTimeStamp()
736
737 def getPullRequestOpenedEvent(self):
738 return self._getPullRequestEvent('opened')
739
740 def getPullRequestSynchronizeEvent(self):
741 return self._getPullRequestEvent('synchronize')
742
743 def getPullRequestReopenedEvent(self):
744 return self._getPullRequestEvent('reopened')
745
746 def getPullRequestClosedEvent(self):
747 return self._getPullRequestEvent('closed')
748
Jesse Keatinga41566f2017-06-14 18:17:51 -0700749 def getPullRequestEditedEvent(self):
750 return self._getPullRequestEvent('edited')
751
Gregory Haynes4fc12542015-04-22 20:38:06 -0700752 def addComment(self, message):
753 self.comments.append(message)
754 self._updateTimeStamp()
755
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200756 def getCommentAddedEvent(self, text):
757 name = 'issue_comment'
758 data = {
759 'action': 'created',
760 'issue': {
761 'number': self.number
762 },
763 'comment': {
764 'body': text
765 },
766 'repository': {
767 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100768 },
769 'sender': {
770 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200771 }
772 }
773 return (name, data)
774
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800775 def getReviewAddedEvent(self, review):
776 name = 'pull_request_review'
777 data = {
778 'action': 'submitted',
779 'pull_request': {
780 'number': self.number,
781 'title': self.subject,
782 'updated_at': self.updated_at,
783 'base': {
784 'ref': self.branch,
785 'repo': {
786 'full_name': self.project
787 }
788 },
789 'head': {
790 'sha': self.head_sha
791 }
792 },
793 'review': {
794 'state': review
795 },
796 'repository': {
797 'full_name': self.project
798 },
799 'sender': {
800 'login': 'ghuser'
801 }
802 }
803 return (name, data)
804
Jan Hruban16ad31f2015-11-07 14:39:07 +0100805 def addLabel(self, name):
806 if name not in self.labels:
807 self.labels.append(name)
808 self._updateTimeStamp()
809 return self._getLabelEvent(name)
810
811 def removeLabel(self, name):
812 if name in self.labels:
813 self.labels.remove(name)
814 self._updateTimeStamp()
815 return self._getUnlabelEvent(name)
816
817 def _getLabelEvent(self, label):
818 name = 'pull_request'
819 data = {
820 'action': 'labeled',
821 'pull_request': {
822 'number': self.number,
823 'updated_at': self.updated_at,
824 'base': {
825 'ref': self.branch,
826 'repo': {
827 'full_name': self.project
828 }
829 },
830 'head': {
831 'sha': self.head_sha
832 }
833 },
834 'label': {
835 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100836 },
837 'sender': {
838 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100839 }
840 }
841 return (name, data)
842
843 def _getUnlabelEvent(self, label):
844 name = 'pull_request'
845 data = {
846 'action': 'unlabeled',
847 'pull_request': {
848 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100849 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100850 'updated_at': self.updated_at,
851 'base': {
852 'ref': self.branch,
853 'repo': {
854 'full_name': self.project
855 }
856 },
857 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800858 'sha': self.head_sha,
859 'repo': {
860 'full_name': self.project
861 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100862 }
863 },
864 'label': {
865 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100866 },
867 'sender': {
868 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100869 }
870 }
871 return (name, data)
872
Jesse Keatinga41566f2017-06-14 18:17:51 -0700873 def editBody(self, body):
874 self.body = body
875 self._updateTimeStamp()
876
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 def _getRepo(self):
878 repo_path = os.path.join(self.upstream_root, self.project)
879 return git.Repo(repo_path)
880
881 def _createPRRef(self):
882 repo = self._getRepo()
883 GithubChangeReference.create(
884 repo, self._getPRReference(), 'refs/tags/init')
885
Jan Hruban570d01c2016-03-10 21:51:32 +0100886 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700887 repo = self._getRepo()
888 ref = repo.references[self._getPRReference()]
889 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100890 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700891 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100892 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700893 repo.head.reference = ref
894 zuul.merger.merger.reset_repo_to_head(repo)
895 repo.git.clean('-x', '-f', '-d')
896
Jan Hruban570d01c2016-03-10 21:51:32 +0100897 if files:
898 fn = files[0]
899 self.files = files
900 else:
901 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
902 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100903 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700904 fn = os.path.join(repo.working_dir, fn)
905 f = open(fn, 'w')
906 with open(fn, 'w') as f:
907 f.write("test %s %s\n" %
908 (self.branch, self.number))
909 repo.index.add([fn])
910
911 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800912 # Create an empty set of statuses for the given sha,
913 # each sha on a PR may have a status set on it
914 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700915 repo.head.reference = 'master'
916 zuul.merger.merger.reset_repo_to_head(repo)
917 repo.git.clean('-x', '-f', '-d')
918 repo.heads['master'].checkout()
919
920 def _updateTimeStamp(self):
921 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
922
923 def getPRHeadSha(self):
924 repo = self._getRepo()
925 return repo.references[self._getPRReference()].commit.hexsha
926
Jesse Keatingae4cd272017-01-30 17:10:44 -0800927 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800928 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
929 # convert the timestamp to a str format that would be returned
930 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800931
Adam Gandelmand81dd762017-02-09 15:15:49 -0800932 if granted_on:
933 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
934 submitted_at = time.strftime(
935 gh_time_format, granted_on.timetuple())
936 else:
937 # github timestamps only down to the second, so we need to make
938 # sure reviews that tests add appear to be added over a period of
939 # time in the past and not all at once.
940 if not self.reviews:
941 # the first review happens 10 mins ago
942 offset = 600
943 else:
944 # subsequent reviews happen 1 minute closer to now
945 offset = 600 - (len(self.reviews) * 60)
946
947 granted_on = datetime.datetime.utcfromtimestamp(
948 time.time() - offset)
949 submitted_at = time.strftime(
950 gh_time_format, granted_on.timetuple())
951
Jesse Keatingae4cd272017-01-30 17:10:44 -0800952 self.reviews.append({
953 'state': state,
954 'user': {
955 'login': user,
956 'email': user + "@derp.com",
957 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800958 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800959 })
960
Gregory Haynes4fc12542015-04-22 20:38:06 -0700961 def _getPRReference(self):
962 return '%s/head' % self.number
963
964 def _getPullRequestEvent(self, action):
965 name = 'pull_request'
966 data = {
967 'action': action,
968 'number': self.number,
969 'pull_request': {
970 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100971 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700972 'updated_at': self.updated_at,
973 'base': {
974 'ref': self.branch,
975 'repo': {
976 'full_name': self.project
977 }
978 },
979 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800980 'sha': self.head_sha,
981 'repo': {
982 'full_name': self.project
983 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700984 },
985 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100986 },
987 'sender': {
988 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700989 }
990 }
991 return (name, data)
992
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800993 def getCommitStatusEvent(self, context, state='success', user='zuul'):
994 name = 'status'
995 data = {
996 'state': state,
997 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700998 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800999 'description': 'Test results for %s: %s' % (self.head_sha, state),
1000 'target_url': 'http://zuul/%s' % self.head_sha,
1001 'branches': [],
1002 'context': context,
1003 'sender': {
1004 'login': user
1005 }
1006 }
1007 return (name, data)
1008
James E. Blair289f5932017-07-27 15:02:29 -07001009 def setMerged(self, commit_message):
1010 self.is_merged = True
1011 self.merge_message = commit_message
1012
1013 repo = self._getRepo()
1014 repo.heads[self.branch].commit = repo.commit(self.head_sha)
1015
Gregory Haynes4fc12542015-04-22 20:38:06 -07001016
1017class FakeGithubConnection(githubconnection.GithubConnection):
1018 log = logging.getLogger("zuul.test.FakeGithubConnection")
1019
1020 def __init__(self, driver, connection_name, connection_config,
1021 upstream_root=None):
1022 super(FakeGithubConnection, self).__init__(driver, connection_name,
1023 connection_config)
1024 self.connection_name = connection_name
1025 self.pr_number = 0
1026 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001027 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001028 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001029 self.merge_failure = False
1030 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001031 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +02001032 self.github_client = FakeGithub()
1033
1034 def getGithubClient(self,
1035 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -06001036 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +02001037 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001038
Jesse Keatinga41566f2017-06-14 18:17:51 -07001039 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001040 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001041 self.pr_number += 1
1042 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001043 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001044 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001045 self.pull_requests.append(pull_request)
1046 return pull_request
1047
Jesse Keating71a47ff2017-06-06 11:36:43 -07001048 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1049 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001050 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001051 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001052 if not new_rev:
1053 new_rev = random_sha1()
1054 name = 'push'
1055 data = {
1056 'ref': ref,
1057 'before': old_rev,
1058 'after': new_rev,
1059 'repository': {
1060 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001061 },
1062 'commits': [
1063 {
1064 'added': added_files,
1065 'removed': removed_files,
1066 'modified': modified_files
1067 }
1068 ]
Wayne1a78c612015-06-11 17:14:13 -07001069 }
1070 return (name, data)
1071
Gregory Haynes4fc12542015-04-22 20:38:06 -07001072 def emitEvent(self, event):
1073 """Emulates sending the GitHub webhook event to the connection."""
1074 port = self.webapp.server.socket.getsockname()[1]
1075 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001076 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001077 secret = self.connection_config['webhook_token']
1078 signature = githubconnection._sign_request(payload, secret)
1079 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001080 req = urllib.request.Request(
1081 'http://localhost:%s/connection/%s/payload'
1082 % (port, self.connection_name),
1083 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001084 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001085
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001086 def addProject(self, project):
1087 # use the original method here and additionally register it in the
1088 # fake github
1089 super(FakeGithubConnection, self).addProject(project)
1090 self.getGithubClient(project).addProject(project)
1091
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001092 def getPull(self, project, number):
1093 pr = self.pull_requests[number - 1]
1094 data = {
1095 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001096 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001097 'updated_at': pr.updated_at,
1098 'base': {
1099 'repo': {
1100 'full_name': pr.project
1101 },
1102 'ref': pr.branch,
1103 },
Jan Hruban37615e52015-11-19 14:30:49 +01001104 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001105 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001106 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001107 'sha': pr.head_sha,
1108 'repo': {
1109 'full_name': pr.project
1110 }
Jesse Keating61040e72017-06-08 15:08:27 -07001111 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001112 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001113 'labels': pr.labels,
1114 'merged': pr.is_merged,
1115 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001116 }
1117 return data
1118
Jesse Keating9021a012017-08-29 14:45:27 -07001119 def getPullBySha(self, sha, project):
1120 prs = list(set([p for p in self.pull_requests if
1121 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001122 if len(prs) > 1:
1123 raise Exception('Multiple pulls found with head sha: %s' % sha)
1124 pr = prs[0]
1125 return self.getPull(pr.project, pr.number)
1126
Jesse Keatingae4cd272017-01-30 17:10:44 -08001127 def _getPullReviews(self, owner, project, number):
1128 pr = self.pull_requests[number - 1]
1129 return pr.reviews
1130
Jesse Keatingae4cd272017-01-30 17:10:44 -08001131 def getRepoPermission(self, project, login):
1132 owner, proj = project.split('/')
1133 for pr in self.pull_requests:
1134 pr_owner, pr_project = pr.project.split('/')
1135 if (pr_owner == owner and proj == pr_project):
1136 if login in pr.writers:
1137 return 'write'
1138 else:
1139 return 'read'
1140
Gregory Haynes4fc12542015-04-22 20:38:06 -07001141 def getGitUrl(self, project):
1142 return os.path.join(self.upstream_root, str(project))
1143
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001144 def real_getGitUrl(self, project):
1145 return super(FakeGithubConnection, self).getGitUrl(project)
1146
Jan Hrubane252a732017-01-03 15:03:09 +01001147 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001148 # record that this got reported
1149 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001150 pull_request = self.pull_requests[pr_number - 1]
1151 pull_request.addComment(message)
1152
Jan Hruban3b415922016-02-03 13:10:22 +01001153 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001154 # record that this got reported
1155 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001156 pull_request = self.pull_requests[pr_number - 1]
1157 if self.merge_failure:
1158 raise Exception('Pull request was not merged')
1159 if self.merge_not_allowed_count > 0:
1160 self.merge_not_allowed_count -= 1
1161 raise MergeFailure('Merge was not successful due to mergeability'
1162 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001163 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001164
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001165 def setCommitStatus(self, project, sha, state, url='', description='',
1166 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001167 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001168 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001169 super(FakeGithubConnection, self).setCommitStatus(
1170 project, sha, state,
1171 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001172
Jan Hruban16ad31f2015-11-07 14:39:07 +01001173 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001174 # record that this got reported
1175 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001176 pull_request = self.pull_requests[pr_number - 1]
1177 pull_request.addLabel(label)
1178
1179 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001180 # record that this got reported
1181 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001182 pull_request = self.pull_requests[pr_number - 1]
1183 pull_request.removeLabel(label)
1184
Jesse Keatinga41566f2017-06-14 18:17:51 -07001185 def _getNeededByFromPR(self, change):
1186 prs = []
1187 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001188 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001189 change.number))
1190 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001191 if not pr.body:
1192 body = ''
1193 else:
1194 body = pr.body
1195 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001196 # Get our version of a pull so that it's a dict
1197 pull = self.getPull(pr.project, pr.number)
1198 prs.append(pull)
1199
1200 return prs
1201
Gregory Haynes4fc12542015-04-22 20:38:06 -07001202
Clark Boylanb640e052014-04-03 16:41:46 -07001203class BuildHistory(object):
1204 def __init__(self, **kw):
1205 self.__dict__.update(kw)
1206
1207 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001208 return ("<Completed build, result: %s name: %s uuid: %s "
1209 "changes: %s ref: %s>" %
1210 (self.result, self.name, self.uuid,
1211 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001212
1213
Clark Boylanb640e052014-04-03 16:41:46 -07001214class FakeStatsd(threading.Thread):
1215 def __init__(self):
1216 threading.Thread.__init__(self)
1217 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001218 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001219 self.sock.bind(('', 0))
1220 self.port = self.sock.getsockname()[1]
1221 self.wake_read, self.wake_write = os.pipe()
1222 self.stats = []
1223
1224 def run(self):
1225 while True:
1226 poll = select.poll()
1227 poll.register(self.sock, select.POLLIN)
1228 poll.register(self.wake_read, select.POLLIN)
1229 ret = poll.poll()
1230 for (fd, event) in ret:
1231 if fd == self.sock.fileno():
1232 data = self.sock.recvfrom(1024)
1233 if not data:
1234 return
1235 self.stats.append(data[0])
1236 if fd == self.wake_read:
1237 return
1238
1239 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001240 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001241
1242
James E. Blaire1767bc2016-08-02 10:00:27 -07001243class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001244 log = logging.getLogger("zuul.test")
1245
Paul Belanger174a8272017-03-14 13:20:10 -04001246 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001247 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001248 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001249 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001250 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001251 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001252 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001253 # TODOv3(jeblair): self.node is really "the label of the node
1254 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001255 # keep using it like this, or we may end up exposing more of
1256 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001257 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001258 self.node = None
1259 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001260 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001261 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001262 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001263 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001264 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001265 self.wait_condition = threading.Condition()
1266 self.waiting = False
1267 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001268 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001269 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001270 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001271 items = self.parameters['zuul']['items']
1272 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1273 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001274
James E. Blair3158e282016-08-19 09:34:11 -07001275 def __repr__(self):
1276 waiting = ''
1277 if self.waiting:
1278 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001279 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1280 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001281
Clark Boylanb640e052014-04-03 16:41:46 -07001282 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001283 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001284 self.wait_condition.acquire()
1285 self.wait_condition.notify()
1286 self.waiting = False
1287 self.log.debug("Build %s released" % self.unique)
1288 self.wait_condition.release()
1289
1290 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001291 """Return whether this build is being held.
1292
1293 :returns: Whether the build is being held.
1294 :rtype: bool
1295 """
1296
Clark Boylanb640e052014-04-03 16:41:46 -07001297 self.wait_condition.acquire()
1298 if self.waiting:
1299 ret = True
1300 else:
1301 ret = False
1302 self.wait_condition.release()
1303 return ret
1304
1305 def _wait(self):
1306 self.wait_condition.acquire()
1307 self.waiting = True
1308 self.log.debug("Build %s waiting" % self.unique)
1309 self.wait_condition.wait()
1310 self.wait_condition.release()
1311
1312 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001313 self.log.debug('Running build %s' % self.unique)
1314
Paul Belanger174a8272017-03-14 13:20:10 -04001315 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001316 self.log.debug('Holding build %s' % self.unique)
1317 self._wait()
1318 self.log.debug("Build %s continuing" % self.unique)
1319
James E. Blair412fba82017-01-26 15:00:50 -08001320 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001321 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001322 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001323 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001324 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001325 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001326 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001327
James E. Blaire1767bc2016-08-02 10:00:27 -07001328 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001329
James E. Blaira5dba232016-08-08 15:53:24 -07001330 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001331 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001332 for change in changes:
1333 if self.hasChanges(change):
1334 return True
1335 return False
1336
James E. Blaire7b99a02016-08-05 14:27:34 -07001337 def hasChanges(self, *changes):
1338 """Return whether this build has certain changes in its git repos.
1339
1340 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001341 are expected to be present (in order) in the git repository of
1342 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001343
1344 :returns: Whether the build has the indicated changes.
1345 :rtype: bool
1346
1347 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001348 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001349 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001350 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001351 try:
1352 repo = git.Repo(path)
1353 except NoSuchPathError as e:
1354 self.log.debug('%s' % e)
1355 return False
James E. Blair247cab72017-07-20 16:52:36 -07001356 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001357 commit_message = '%s-1' % change.subject
1358 self.log.debug("Checking if build %s has changes; commit_message "
1359 "%s; repo_messages %s" % (self, commit_message,
1360 repo_messages))
1361 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001362 self.log.debug(" messages do not match")
1363 return False
1364 self.log.debug(" OK")
1365 return True
1366
James E. Blaird8af5422017-05-24 13:59:40 -07001367 def getWorkspaceRepos(self, projects):
1368 """Return workspace git repo objects for the listed projects
1369
1370 :arg list projects: A list of strings, each the canonical name
1371 of a project.
1372
1373 :returns: A dictionary of {name: repo} for every listed
1374 project.
1375 :rtype: dict
1376
1377 """
1378
1379 repos = {}
1380 for project in projects:
1381 path = os.path.join(self.jobdir.src_root, project)
1382 repo = git.Repo(path)
1383 repos[project] = repo
1384 return repos
1385
Clark Boylanb640e052014-04-03 16:41:46 -07001386
James E. Blair107bb252017-10-13 15:53:16 -07001387class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1388 def doMergeChanges(self, merger, items, repo_state):
1389 # Get a merger in order to update the repos involved in this job.
1390 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1391 merger, items, repo_state)
1392 if not commit: # merge conflict
1393 self.recordResult('MERGER_FAILURE')
1394 return commit
1395
1396 def recordResult(self, result):
1397 build = self.executor_server.job_builds[self.job.unique]
1398 self.executor_server.lock.acquire()
1399 self.executor_server.build_history.append(
1400 BuildHistory(name=build.name, result=result, changes=build.changes,
1401 node=build.node, uuid=build.unique,
1402 ref=build.parameters['zuul']['ref'],
1403 parameters=build.parameters, jobdir=build.jobdir,
1404 pipeline=build.parameters['zuul']['pipeline'])
1405 )
1406 self.executor_server.running_builds.remove(build)
1407 del self.executor_server.job_builds[self.job.unique]
1408 self.executor_server.lock.release()
1409
1410 def runPlaybooks(self, args):
1411 build = self.executor_server.job_builds[self.job.unique]
1412 build.jobdir = self.jobdir
1413
1414 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1415 self.recordResult(result)
1416 return result
1417
James E. Blaira86aaf12017-10-15 20:59:50 -07001418 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001419 build = self.executor_server.job_builds[self.job.unique]
1420
1421 if self.executor_server._run_ansible:
1422 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001423 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001424 else:
1425 if playbook.path:
1426 result = build.run()
1427 else:
1428 result = (self.RESULT_NORMAL, 0)
1429 return result
1430
1431 def getHostList(self, args):
1432 self.log.debug("hostlist")
1433 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1434 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001435 if not host['host_vars'].get('ansible_connection'):
1436 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001437
1438 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001439 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001440 host_vars=dict(ansible_connection='local'),
1441 host_keys=[]))
1442 return hosts
1443
1444
Paul Belanger174a8272017-03-14 13:20:10 -04001445class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1446 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001447
Paul Belanger174a8272017-03-14 13:20:10 -04001448 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001449 they will report that they have started but then pause until
1450 released before reporting completion. This attribute may be
1451 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001452 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001453 be explicitly released.
1454
1455 """
James E. Blairfaf81982017-10-10 15:42:26 -07001456
1457 _job_class = RecordingAnsibleJob
1458
James E. Blairf5dbd002015-12-23 15:26:17 -08001459 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001460 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001461 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001462 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001463 self.hold_jobs_in_build = False
1464 self.lock = threading.Lock()
1465 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001466 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001467 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001468 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001469
James E. Blaira5dba232016-08-08 15:53:24 -07001470 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001471 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001472
1473 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001474 :arg Change change: The :py:class:`~tests.base.FakeChange`
1475 instance which should cause the job to fail. This job
1476 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001477
1478 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001479 l = self.fail_tests.get(name, [])
1480 l.append(change)
1481 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001482
James E. Blair962220f2016-08-03 11:22:38 -07001483 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001484 """Release a held build.
1485
1486 :arg str regex: A regular expression which, if supplied, will
1487 cause only builds with matching names to be released. If
1488 not supplied, all builds will be released.
1489
1490 """
James E. Blair962220f2016-08-03 11:22:38 -07001491 builds = self.running_builds[:]
1492 self.log.debug("Releasing build %s (%s)" % (regex,
1493 len(self.running_builds)))
1494 for build in builds:
1495 if not regex or re.match(regex, build.name):
1496 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001497 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001498 build.release()
1499 else:
1500 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001501 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001502 self.log.debug("Done releasing builds %s (%s)" %
1503 (regex, len(self.running_builds)))
1504
Paul Belanger174a8272017-03-14 13:20:10 -04001505 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001506 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001507 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001508 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001509 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001510 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001511 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001512 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001513 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001514
1515 def stopJob(self, job):
1516 self.log.debug("handle stop")
1517 parameters = json.loads(job.arguments)
1518 uuid = parameters['uuid']
1519 for build in self.running_builds:
1520 if build.unique == uuid:
1521 build.aborted = True
1522 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001523 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001524
James E. Blaira002b032017-04-18 10:35:48 -07001525 def stop(self):
1526 for build in self.running_builds:
1527 build.release()
1528 super(RecordingExecutorServer, self).stop()
1529
Joshua Hesketh50c21782016-10-13 21:34:14 +11001530
Clark Boylanb640e052014-04-03 16:41:46 -07001531class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001532 """A Gearman server for use in tests.
1533
1534 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1535 added to the queue but will not be distributed to workers
1536 until released. This attribute may be changed at any time and
1537 will take effect for subsequently enqueued jobs, but
1538 previously held jobs will still need to be explicitly
1539 released.
1540
1541 """
1542
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001543 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001544 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001545 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001546 if use_ssl:
1547 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1548 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1549 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1550 else:
1551 ssl_ca = None
1552 ssl_cert = None
1553 ssl_key = None
1554
1555 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1556 ssl_cert=ssl_cert,
1557 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001558
1559 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001560 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1561 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001562 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001563 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001564 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001565 elif job.name.startswith(b'merger:'):
1566 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001567 else:
1568 job.waiting = False
1569 if job.waiting:
1570 continue
1571 if job.name in connection.functions:
1572 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001573 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001574 connection.related_jobs[job.handle] = job
1575 job.worker_connection = connection
1576 job.running = True
1577 return job
1578 return None
1579
1580 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001581 """Release a held job.
1582
1583 :arg str regex: A regular expression which, if supplied, will
1584 cause only jobs with matching names to be released. If
1585 not supplied, all jobs will be released.
1586 """
Clark Boylanb640e052014-04-03 16:41:46 -07001587 released = False
1588 qlen = (len(self.high_queue) + len(self.normal_queue) +
1589 len(self.low_queue))
1590 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1591 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001592 match = False
1593 if job.name == b'executor:execute':
1594 parameters = json.loads(job.arguments.decode('utf8'))
1595 if not regex or re.match(regex, parameters.get('job')):
1596 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001597 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001598 if not regex:
1599 match = True
1600 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001601 self.log.debug("releasing queued job %s" %
1602 job.unique)
1603 job.waiting = False
1604 released = True
1605 else:
1606 self.log.debug("not releasing queued job %s" %
1607 job.unique)
1608 if released:
1609 self.wakeConnections()
1610 qlen = (len(self.high_queue) + len(self.normal_queue) +
1611 len(self.low_queue))
1612 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1613
1614
1615class FakeSMTP(object):
1616 log = logging.getLogger('zuul.FakeSMTP')
1617
1618 def __init__(self, messages, server, port):
1619 self.server = server
1620 self.port = port
1621 self.messages = messages
1622
1623 def sendmail(self, from_email, to_email, msg):
1624 self.log.info("Sending email from %s, to %s, with msg %s" % (
1625 from_email, to_email, msg))
1626
1627 headers = msg.split('\n\n', 1)[0]
1628 body = msg.split('\n\n', 1)[1]
1629
1630 self.messages.append(dict(
1631 from_email=from_email,
1632 to_email=to_email,
1633 msg=msg,
1634 headers=headers,
1635 body=body,
1636 ))
1637
1638 return True
1639
1640 def quit(self):
1641 return True
1642
1643
James E. Blairdce6cea2016-12-20 16:45:32 -08001644class FakeNodepool(object):
1645 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001646 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001647
1648 log = logging.getLogger("zuul.test.FakeNodepool")
1649
1650 def __init__(self, host, port, chroot):
1651 self.client = kazoo.client.KazooClient(
1652 hosts='%s:%s%s' % (host, port, chroot))
1653 self.client.start()
1654 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001655 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001656 self.thread = threading.Thread(target=self.run)
1657 self.thread.daemon = True
1658 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001659 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001660
1661 def stop(self):
1662 self._running = False
1663 self.thread.join()
1664 self.client.stop()
1665 self.client.close()
1666
1667 def run(self):
1668 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001669 try:
1670 self._run()
1671 except Exception:
1672 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001673 time.sleep(0.1)
1674
1675 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001676 if self.paused:
1677 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001678 for req in self.getNodeRequests():
1679 self.fulfillRequest(req)
1680
1681 def getNodeRequests(self):
1682 try:
1683 reqids = self.client.get_children(self.REQUEST_ROOT)
1684 except kazoo.exceptions.NoNodeError:
1685 return []
1686 reqs = []
1687 for oid in sorted(reqids):
1688 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001689 try:
1690 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001691 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001692 data['_oid'] = oid
1693 reqs.append(data)
1694 except kazoo.exceptions.NoNodeError:
1695 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001696 return reqs
1697
James E. Blaire18d4602017-01-05 11:17:28 -08001698 def getNodes(self):
1699 try:
1700 nodeids = self.client.get_children(self.NODE_ROOT)
1701 except kazoo.exceptions.NoNodeError:
1702 return []
1703 nodes = []
1704 for oid in sorted(nodeids):
1705 path = self.NODE_ROOT + '/' + oid
1706 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001707 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001708 data['_oid'] = oid
1709 try:
1710 lockfiles = self.client.get_children(path + '/lock')
1711 except kazoo.exceptions.NoNodeError:
1712 lockfiles = []
1713 if lockfiles:
1714 data['_lock'] = True
1715 else:
1716 data['_lock'] = False
1717 nodes.append(data)
1718 return nodes
1719
James E. Blaira38c28e2017-01-04 10:33:20 -08001720 def makeNode(self, request_id, node_type):
1721 now = time.time()
1722 path = '/nodepool/nodes/'
1723 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001724 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001725 provider='test-provider',
1726 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001727 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001728 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001729 public_ipv4='127.0.0.1',
1730 private_ipv4=None,
1731 public_ipv6=None,
1732 allocated_to=request_id,
1733 state='ready',
1734 state_time=now,
1735 created_time=now,
1736 updated_time=now,
1737 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001738 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001739 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001740 if 'fakeuser' in node_type:
1741 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001742 if 'windows' in node_type:
1743 data['connection_type'] = 'winrm'
1744
Clint Byrumf322fe22017-05-10 20:53:12 -07001745 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001746 path = self.client.create(path, data,
1747 makepath=True,
1748 sequence=True)
1749 nodeid = path.split("/")[-1]
1750 return nodeid
1751
James E. Blair6ab79e02017-01-06 10:10:17 -08001752 def addFailRequest(self, request):
1753 self.fail_requests.add(request['_oid'])
1754
James E. Blairdce6cea2016-12-20 16:45:32 -08001755 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001756 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001757 return
1758 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001759 oid = request['_oid']
1760 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001761
James E. Blair6ab79e02017-01-06 10:10:17 -08001762 if oid in self.fail_requests:
1763 request['state'] = 'failed'
1764 else:
1765 request['state'] = 'fulfilled'
1766 nodes = []
1767 for node in request['node_types']:
1768 nodeid = self.makeNode(oid, node)
1769 nodes.append(nodeid)
1770 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001771
James E. Blaira38c28e2017-01-04 10:33:20 -08001772 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001773 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001774 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001775 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001776 try:
1777 self.client.set(path, data)
1778 except kazoo.exceptions.NoNodeError:
1779 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001780
1781
James E. Blair498059b2016-12-20 13:50:13 -08001782class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001783 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001784 super(ChrootedKazooFixture, self).__init__()
1785
1786 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1787 if ':' in zk_host:
1788 host, port = zk_host.split(':')
1789 else:
1790 host = zk_host
1791 port = None
1792
1793 self.zookeeper_host = host
1794
1795 if not port:
1796 self.zookeeper_port = 2181
1797 else:
1798 self.zookeeper_port = int(port)
1799
Clark Boylan621ec9a2017-04-07 17:41:33 -07001800 self.test_id = test_id
1801
James E. Blair498059b2016-12-20 13:50:13 -08001802 def _setUp(self):
1803 # Make sure the test chroot paths do not conflict
1804 random_bits = ''.join(random.choice(string.ascii_lowercase +
1805 string.ascii_uppercase)
1806 for x in range(8))
1807
Clark Boylan621ec9a2017-04-07 17:41:33 -07001808 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001809 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1810
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001811 self.addCleanup(self._cleanup)
1812
James E. Blair498059b2016-12-20 13:50:13 -08001813 # Ensure the chroot path exists and clean up any pre-existing znodes.
1814 _tmp_client = kazoo.client.KazooClient(
1815 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1816 _tmp_client.start()
1817
1818 if _tmp_client.exists(self.zookeeper_chroot):
1819 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1820
1821 _tmp_client.ensure_path(self.zookeeper_chroot)
1822 _tmp_client.stop()
1823 _tmp_client.close()
1824
James E. Blair498059b2016-12-20 13:50:13 -08001825 def _cleanup(self):
1826 '''Remove the chroot path.'''
1827 # Need a non-chroot'ed client to remove the chroot path
1828 _tmp_client = kazoo.client.KazooClient(
1829 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1830 _tmp_client.start()
1831 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1832 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001833 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001834
1835
Joshua Heskethd78b4482015-09-14 16:56:34 -06001836class MySQLSchemaFixture(fixtures.Fixture):
1837 def setUp(self):
1838 super(MySQLSchemaFixture, self).setUp()
1839
1840 random_bits = ''.join(random.choice(string.ascii_lowercase +
1841 string.ascii_uppercase)
1842 for x in range(8))
1843 self.name = '%s_%s' % (random_bits, os.getpid())
1844 self.passwd = uuid.uuid4().hex
1845 db = pymysql.connect(host="localhost",
1846 user="openstack_citest",
1847 passwd="openstack_citest",
1848 db="openstack_citest")
1849 cur = db.cursor()
1850 cur.execute("create database %s" % self.name)
1851 cur.execute(
1852 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1853 (self.name, self.name, self.passwd))
1854 cur.execute("flush privileges")
1855
1856 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1857 self.passwd,
1858 self.name)
1859 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1860 self.addCleanup(self.cleanup)
1861
1862 def cleanup(self):
1863 db = pymysql.connect(host="localhost",
1864 user="openstack_citest",
1865 passwd="openstack_citest",
1866 db="openstack_citest")
1867 cur = db.cursor()
1868 cur.execute("drop database %s" % self.name)
1869 cur.execute("drop user '%s'@'localhost'" % self.name)
1870 cur.execute("flush privileges")
1871
1872
Maru Newby3fe5f852015-01-13 04:22:14 +00001873class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001874 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001875 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001876
James E. Blair1c236df2017-02-01 14:07:24 -08001877 def attachLogs(self, *args):
1878 def reader():
1879 self._log_stream.seek(0)
1880 while True:
1881 x = self._log_stream.read(4096)
1882 if not x:
1883 break
1884 yield x.encode('utf8')
1885 content = testtools.content.content_from_reader(
1886 reader,
1887 testtools.content_type.UTF8_TEXT,
1888 False)
1889 self.addDetail('logging', content)
1890
Clark Boylanb640e052014-04-03 16:41:46 -07001891 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001892 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001893 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1894 try:
1895 test_timeout = int(test_timeout)
1896 except ValueError:
1897 # If timeout value is invalid do not set a timeout.
1898 test_timeout = 0
1899 if test_timeout > 0:
1900 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1901
1902 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1903 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1904 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1905 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1906 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1907 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1908 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1909 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1910 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1911 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001912 self._log_stream = StringIO()
1913 self.addOnException(self.attachLogs)
1914 else:
1915 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001916
James E. Blair73b41772017-05-22 13:22:55 -07001917 # NOTE(jeblair): this is temporary extra debugging to try to
1918 # track down a possible leak.
1919 orig_git_repo_init = git.Repo.__init__
1920
1921 def git_repo_init(myself, *args, **kw):
1922 orig_git_repo_init(myself, *args, **kw)
1923 self.log.debug("Created git repo 0x%x %s" %
1924 (id(myself), repr(myself)))
1925
1926 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1927 git_repo_init))
1928
James E. Blair1c236df2017-02-01 14:07:24 -08001929 handler = logging.StreamHandler(self._log_stream)
1930 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1931 '%(levelname)-8s %(message)s')
1932 handler.setFormatter(formatter)
1933
1934 logger = logging.getLogger()
1935 logger.setLevel(logging.DEBUG)
1936 logger.addHandler(handler)
1937
Clark Boylan3410d532017-04-25 12:35:29 -07001938 # Make sure we don't carry old handlers around in process state
1939 # which slows down test runs
1940 self.addCleanup(logger.removeHandler, handler)
1941 self.addCleanup(handler.close)
1942 self.addCleanup(handler.flush)
1943
James E. Blair1c236df2017-02-01 14:07:24 -08001944 # NOTE(notmorgan): Extract logging overrides for specific
1945 # libraries from the OS_LOG_DEFAULTS env and create loggers
1946 # for each. This is used to limit the output during test runs
1947 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001948 log_defaults_from_env = os.environ.get(
1949 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001950 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001951
James E. Blairdce6cea2016-12-20 16:45:32 -08001952 if log_defaults_from_env:
1953 for default in log_defaults_from_env.split(','):
1954 try:
1955 name, level_str = default.split('=', 1)
1956 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001957 logger = logging.getLogger(name)
1958 logger.setLevel(level)
1959 logger.addHandler(handler)
1960 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001961 except ValueError:
1962 # NOTE(notmorgan): Invalid format of the log default,
1963 # skip and don't try and apply a logger for the
1964 # specified module
1965 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001966
Maru Newby3fe5f852015-01-13 04:22:14 +00001967
1968class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001969 """A test case with a functioning Zuul.
1970
1971 The following class variables are used during test setup and can
1972 be overidden by subclasses but are effectively read-only once a
1973 test method starts running:
1974
1975 :cvar str config_file: This points to the main zuul config file
1976 within the fixtures directory. Subclasses may override this
1977 to obtain a different behavior.
1978
1979 :cvar str tenant_config_file: This is the tenant config file
1980 (which specifies from what git repos the configuration should
1981 be loaded). It defaults to the value specified in
1982 `config_file` but can be overidden by subclasses to obtain a
1983 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001984 configuration. See also the :py:func:`simple_layout`
1985 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001986
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001987 :cvar bool create_project_keys: Indicates whether Zuul should
1988 auto-generate keys for each project, or whether the test
1989 infrastructure should insert dummy keys to save time during
1990 startup. Defaults to False.
1991
James E. Blaire7b99a02016-08-05 14:27:34 -07001992 The following are instance variables that are useful within test
1993 methods:
1994
1995 :ivar FakeGerritConnection fake_<connection>:
1996 A :py:class:`~tests.base.FakeGerritConnection` will be
1997 instantiated for each connection present in the config file
1998 and stored here. For instance, `fake_gerrit` will hold the
1999 FakeGerritConnection object for a connection named `gerrit`.
2000
2001 :ivar FakeGearmanServer gearman_server: An instance of
2002 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
2003 server that all of the Zuul components in this test use to
2004 communicate with each other.
2005
Paul Belanger174a8272017-03-14 13:20:10 -04002006 :ivar RecordingExecutorServer executor_server: An instance of
2007 :py:class:`~tests.base.RecordingExecutorServer` which is the
2008 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07002009
2010 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
2011 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04002012 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07002013 list upon completion.
2014
2015 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
2016 objects representing completed builds. They are appended to
2017 the list in the order they complete.
2018
2019 """
2020
James E. Blair83005782015-12-11 14:46:03 -08002021 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07002022 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002023 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002024 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07002025
2026 def _startMerger(self):
2027 self.merge_server = zuul.merger.server.MergeServer(self.config,
2028 self.connections)
2029 self.merge_server.start()
2030
Maru Newby3fe5f852015-01-13 04:22:14 +00002031 def setUp(self):
2032 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08002033
2034 self.setupZK()
2035
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002036 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07002037 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002038 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2039 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002040 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002041 tmp_root = tempfile.mkdtemp(
2042 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002043 self.test_root = os.path.join(tmp_root, "zuul-test")
2044 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002045 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002046 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002047 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002048 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2049 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002050
2051 if os.path.exists(self.test_root):
2052 shutil.rmtree(self.test_root)
2053 os.makedirs(self.test_root)
2054 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002055 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002056 os.makedirs(self.merger_state_root)
2057 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002058
2059 # Make per test copy of Configuration.
2060 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002061 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2062 if not os.path.exists(self.private_key_file):
2063 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2064 shutil.copy(src_private_key_file, self.private_key_file)
2065 shutil.copy('{}.pub'.format(src_private_key_file),
2066 '{}.pub'.format(self.private_key_file))
2067 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002068 self.config.set('scheduler', 'tenant_config',
2069 os.path.join(
2070 FIXTURE_DIR,
2071 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002072 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05002073 self.config.set(
2074 'scheduler', 'command_socket',
2075 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05002076 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002077 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002078 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002079 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05002080 self.config.set(
2081 'executor', 'command_socket',
2082 os.path.join(self.test_root, 'executor.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07002083
Clark Boylanb640e052014-04-03 16:41:46 -07002084 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07002085 if self.config.has_section('statsd'):
2086 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07002087 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002088
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002089 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002090
2091 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002092 self.log.info("Gearman server on port %s" %
2093 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002094 if self.use_ssl:
2095 self.log.info('SSL enabled for gearman')
2096 self.config.set(
2097 'gearman', 'ssl_ca',
2098 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2099 self.config.set(
2100 'gearman', 'ssl_cert',
2101 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2102 self.config.set(
2103 'gearman', 'ssl_key',
2104 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002105
James E. Blaire511d2f2016-12-08 15:22:26 -08002106 gerritsource.GerritSource.replication_timeout = 1.5
2107 gerritsource.GerritSource.replication_retry_interval = 0.5
2108 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002109
Joshua Hesketh352264b2015-08-11 23:42:08 +10002110 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002111 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002112
Jan Hruban7083edd2015-08-21 14:00:54 +02002113 self.webapp = zuul.webapp.WebApp(
2114 self.sched, port=0, listen_address='127.0.0.1')
2115
Jan Hruban6b71aff2015-10-22 16:58:08 +02002116 self.event_queues = [
2117 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002118 self.sched.trigger_event_queue,
2119 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002120 ]
2121
James E. Blairfef78942016-03-11 16:28:56 -08002122 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002123 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002124
Paul Belanger174a8272017-03-14 13:20:10 -04002125 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002126 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002127 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002128 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002129 _test_root=self.test_root,
2130 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002131 self.executor_server.start()
2132 self.history = self.executor_server.build_history
2133 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002134
Paul Belanger174a8272017-03-14 13:20:10 -04002135 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002136 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002137 self.merge_client = zuul.merger.client.MergeClient(
2138 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002139 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002140 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002141 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002142
James E. Blair0d5a36e2017-02-21 10:53:44 -05002143 self.fake_nodepool = FakeNodepool(
2144 self.zk_chroot_fixture.zookeeper_host,
2145 self.zk_chroot_fixture.zookeeper_port,
2146 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002147
Paul Belanger174a8272017-03-14 13:20:10 -04002148 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002149 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002150 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002151 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002152
Clark Boylanb640e052014-04-03 16:41:46 -07002153 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002154 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002155 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002156 # Cleanups are run in reverse order
2157 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002158 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002159 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002160
James E. Blairb9c0d772017-03-03 14:34:49 -08002161 self.sched.reconfigure(self.config)
2162 self.sched.resume()
2163
Tobias Henkel7df274b2017-05-26 17:41:11 +02002164 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002165 # Set up gerrit related fakes
2166 # Set a changes database so multiple FakeGerrit's can report back to
2167 # a virtual canonical database given by the configured hostname
2168 self.gerrit_changes_dbs = {}
2169
2170 def getGerritConnection(driver, name, config):
2171 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2172 con = FakeGerritConnection(driver, name, config,
2173 changes_db=db,
2174 upstream_root=self.upstream_root)
2175 self.event_queues.append(con.event_queue)
2176 setattr(self, 'fake_' + name, con)
2177 return con
2178
2179 self.useFixture(fixtures.MonkeyPatch(
2180 'zuul.driver.gerrit.GerritDriver.getConnection',
2181 getGerritConnection))
2182
Gregory Haynes4fc12542015-04-22 20:38:06 -07002183 def getGithubConnection(driver, name, config):
2184 con = FakeGithubConnection(driver, name, config,
2185 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002186 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002187 setattr(self, 'fake_' + name, con)
2188 return con
2189
2190 self.useFixture(fixtures.MonkeyPatch(
2191 'zuul.driver.github.GithubDriver.getConnection',
2192 getGithubConnection))
2193
James E. Blaire511d2f2016-12-08 15:22:26 -08002194 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002195 # TODO(jhesketh): This should come from lib.connections for better
2196 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002197 # Register connections from the config
2198 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002199
Joshua Hesketh352264b2015-08-11 23:42:08 +10002200 def FakeSMTPFactory(*args, **kw):
2201 args = [self.smtp_messages] + list(args)
2202 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002203
Joshua Hesketh352264b2015-08-11 23:42:08 +10002204 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002205
James E. Blaire511d2f2016-12-08 15:22:26 -08002206 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002207 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002208 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002209
James E. Blair83005782015-12-11 14:46:03 -08002210 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002211 # This creates the per-test configuration object. It can be
2212 # overriden by subclasses, but should not need to be since it
2213 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002214 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002215 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002216
James E. Blair39840362017-06-23 20:34:02 +01002217 sections = ['zuul', 'scheduler', 'executor', 'merger']
2218 for section in sections:
2219 if not self.config.has_section(section):
2220 self.config.add_section(section)
2221
James E. Blair06cc3922017-04-19 10:08:10 -07002222 if not self.setupSimpleLayout():
2223 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002224 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002225 self.tenant_config_file)
2226 git_path = os.path.join(
2227 os.path.dirname(
2228 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2229 'git')
2230 if os.path.exists(git_path):
2231 for reponame in os.listdir(git_path):
2232 project = reponame.replace('_', '/')
2233 self.copyDirToRepo(project,
2234 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002235 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002236 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002237 self.setupAllProjectKeys()
2238
James E. Blair06cc3922017-04-19 10:08:10 -07002239 def setupSimpleLayout(self):
2240 # If the test method has been decorated with a simple_layout,
2241 # use that instead of the class tenant_config_file. Set up a
2242 # single config-project with the specified layout, and
2243 # initialize repos for all of the 'project' entries which
2244 # appear in the layout.
2245 test_name = self.id().split('.')[-1]
2246 test = getattr(self, test_name)
2247 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002248 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002249 else:
2250 return False
2251
James E. Blairb70e55a2017-04-19 12:57:02 -07002252 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002253 path = os.path.join(FIXTURE_DIR, path)
2254 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002255 data = f.read()
2256 layout = yaml.safe_load(data)
2257 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002258 untrusted_projects = []
2259 for item in layout:
2260 if 'project' in item:
2261 name = item['project']['name']
2262 untrusted_projects.append(name)
2263 self.init_repo(name)
2264 self.addCommitToRepo(name, 'initial commit',
2265 files={'README': ''},
2266 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002267 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002268 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002269 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002270 for fn in zuul.configloader.as_list(
2271 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002272 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002273 for fn in zuul.configloader.as_list(
2274 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002275 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002276
2277 root = os.path.join(self.test_root, "config")
2278 if not os.path.exists(root):
2279 os.makedirs(root)
2280 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2281 config = [{'tenant':
2282 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002283 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002284 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002285 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002286 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002287 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002288 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002289 os.path.join(FIXTURE_DIR, f.name))
2290
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002291 self.init_repo('org/common-config')
2292 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002293 files, branch='master', tag='init')
2294
2295 return True
2296
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002297 def setupAllProjectKeys(self):
2298 if self.create_project_keys:
2299 return
2300
James E. Blair39840362017-06-23 20:34:02 +01002301 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002302 with open(os.path.join(FIXTURE_DIR, path)) as f:
2303 tenant_config = yaml.safe_load(f.read())
2304 for tenant in tenant_config:
2305 sources = tenant['tenant']['source']
2306 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002307 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002308 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002309 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002310 self.setupProjectKeys(source, project)
2311
2312 def setupProjectKeys(self, source, project):
2313 # Make sure we set up an RSA key for the project so that we
2314 # don't spend time generating one:
2315
James E. Blair6459db12017-06-29 14:57:20 -07002316 if isinstance(project, dict):
2317 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002318 key_root = os.path.join(self.state_root, 'keys')
2319 if not os.path.isdir(key_root):
2320 os.mkdir(key_root, 0o700)
2321 private_key_file = os.path.join(key_root, source, project + '.pem')
2322 private_key_dir = os.path.dirname(private_key_file)
2323 self.log.debug("Installing test keys for project %s at %s" % (
2324 project, private_key_file))
2325 if not os.path.isdir(private_key_dir):
2326 os.makedirs(private_key_dir)
2327 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2328 with open(private_key_file, 'w') as o:
2329 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002330
James E. Blair498059b2016-12-20 13:50:13 -08002331 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002332 self.zk_chroot_fixture = self.useFixture(
2333 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002334 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002335 self.zk_chroot_fixture.zookeeper_host,
2336 self.zk_chroot_fixture.zookeeper_port,
2337 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002338
James E. Blair96c6bf82016-01-15 16:20:40 -08002339 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002340 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002341
2342 files = {}
2343 for (dirpath, dirnames, filenames) in os.walk(source_path):
2344 for filename in filenames:
2345 test_tree_filepath = os.path.join(dirpath, filename)
2346 common_path = os.path.commonprefix([test_tree_filepath,
2347 source_path])
2348 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2349 with open(test_tree_filepath, 'r') as f:
2350 content = f.read()
2351 files[relative_filepath] = content
2352 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002353 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002354
James E. Blaire18d4602017-01-05 11:17:28 -08002355 def assertNodepoolState(self):
2356 # Make sure that there are no pending requests
2357
2358 requests = self.fake_nodepool.getNodeRequests()
2359 self.assertEqual(len(requests), 0)
2360
2361 nodes = self.fake_nodepool.getNodes()
2362 for node in nodes:
2363 self.assertFalse(node['_lock'], "Node %s is locked" %
2364 (node['_oid'],))
2365
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002366 def assertNoGeneratedKeys(self):
2367 # Make sure that Zuul did not generate any project keys
2368 # (unless it was supposed to).
2369
2370 if self.create_project_keys:
2371 return
2372
2373 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2374 test_key = i.read()
2375
2376 key_root = os.path.join(self.state_root, 'keys')
2377 for root, dirname, files in os.walk(key_root):
2378 for fn in files:
2379 with open(os.path.join(root, fn)) as f:
2380 self.assertEqual(test_key, f.read())
2381
Clark Boylanb640e052014-04-03 16:41:46 -07002382 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002383 self.log.debug("Assert final state")
2384 # Make sure no jobs are running
2385 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002386 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002387 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002388 gc.collect()
2389 for obj in gc.get_objects():
2390 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002391 self.log.debug("Leaked git repo object: 0x%x %s" %
2392 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002393 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002394 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002395 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002396 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002397 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002398 for tenant in self.sched.abide.tenants.values():
2399 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002400 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002401 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002402
2403 def shutdown(self):
2404 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002405 self.executor_server.hold_jobs_in_build = False
2406 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002407 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002408 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002409 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002410 self.sched.stop()
2411 self.sched.join()
2412 self.statsd.stop()
2413 self.statsd.join()
2414 self.webapp.stop()
2415 self.webapp.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002416 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002417 self.fake_nodepool.stop()
2418 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002419 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002420 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002421 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002422 # Further the pydevd threads also need to be whitelisted so debugging
2423 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002424 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002425 'pydevd.CommandThread',
2426 'pydevd.Reader',
2427 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002428 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002429 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002430 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002431 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002432 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002433 log_str = ""
2434 for thread_id, stack_frame in sys._current_frames().items():
2435 log_str += "Thread: %s\n" % thread_id
2436 log_str += "".join(traceback.format_stack(stack_frame))
2437 self.log.debug(log_str)
2438 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002439
James E. Blaira002b032017-04-18 10:35:48 -07002440 def assertCleanShutdown(self):
2441 pass
2442
James E. Blairc4ba97a2017-04-19 16:26:24 -07002443 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002444 parts = project.split('/')
2445 path = os.path.join(self.upstream_root, *parts[:-1])
2446 if not os.path.exists(path):
2447 os.makedirs(path)
2448 path = os.path.join(self.upstream_root, project)
2449 repo = git.Repo.init(path)
2450
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002451 with repo.config_writer() as config_writer:
2452 config_writer.set_value('user', 'email', 'user@example.com')
2453 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002454
Clark Boylanb640e052014-04-03 16:41:46 -07002455 repo.index.commit('initial commit')
2456 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002457 if tag:
2458 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002459
James E. Blair97d902e2014-08-21 13:25:56 -07002460 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002461 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002462 repo.git.clean('-x', '-f', '-d')
2463
James E. Blair97d902e2014-08-21 13:25:56 -07002464 def create_branch(self, project, branch):
2465 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002466 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002467 fn = os.path.join(path, 'README')
2468
2469 branch_head = repo.create_head(branch)
2470 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002471 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002472 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002473 f.close()
2474 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002475 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002476
James E. Blair97d902e2014-08-21 13:25:56 -07002477 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002478 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002479 repo.git.clean('-x', '-f', '-d')
2480
Sachi King9f16d522016-03-16 12:20:45 +11002481 def create_commit(self, project):
2482 path = os.path.join(self.upstream_root, project)
2483 repo = git.Repo(path)
2484 repo.head.reference = repo.heads['master']
2485 file_name = os.path.join(path, 'README')
2486 with open(file_name, 'a') as f:
2487 f.write('creating fake commit\n')
2488 repo.index.add([file_name])
2489 commit = repo.index.commit('Creating a fake commit')
2490 return commit.hexsha
2491
James E. Blairf4a5f022017-04-18 14:01:10 -07002492 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002493 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002494 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002495 while len(self.builds):
2496 self.release(self.builds[0])
2497 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002498 i += 1
2499 if count is not None and i >= count:
2500 break
James E. Blairb8c16472015-05-05 14:55:26 -07002501
James E. Blairdf25ddc2017-07-08 07:57:09 -07002502 def getSortedBuilds(self):
2503 "Return the list of currently running builds sorted by name"
2504
2505 return sorted(self.builds, key=lambda x: x.name)
2506
Clark Boylanb640e052014-04-03 16:41:46 -07002507 def release(self, job):
2508 if isinstance(job, FakeBuild):
2509 job.release()
2510 else:
2511 job.waiting = False
2512 self.log.debug("Queued job %s released" % job.unique)
2513 self.gearman_server.wakeConnections()
2514
2515 def getParameter(self, job, name):
2516 if isinstance(job, FakeBuild):
2517 return job.parameters[name]
2518 else:
2519 parameters = json.loads(job.arguments)
2520 return parameters[name]
2521
Clark Boylanb640e052014-04-03 16:41:46 -07002522 def haveAllBuildsReported(self):
2523 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002524 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002525 return False
2526 # Find out if every build that the worker has completed has been
2527 # reported back to Zuul. If it hasn't then that means a Gearman
2528 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002529 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002530 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002531 if not zbuild:
2532 # It has already been reported
2533 continue
2534 # It hasn't been reported yet.
2535 return False
2536 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002537 worker = self.executor_server.executor_worker
2538 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002539 if connection.state == 'GRAB_WAIT':
2540 return False
2541 return True
2542
2543 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002544 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002545 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002546 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002547 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002548 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002549 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002550 for j in conn.related_jobs.values():
2551 if j.unique == build.uuid:
2552 client_job = j
2553 break
2554 if not client_job:
2555 self.log.debug("%s is not known to the gearman client" %
2556 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002557 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002558 if not client_job.handle:
2559 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002560 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002561 server_job = self.gearman_server.jobs.get(client_job.handle)
2562 if not server_job:
2563 self.log.debug("%s is not known to the gearman server" %
2564 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002565 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002566 if not hasattr(server_job, 'waiting'):
2567 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002568 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002569 if server_job.waiting:
2570 continue
James E. Blair17302972016-08-10 16:11:42 -07002571 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002572 self.log.debug("%s has not reported start" % build)
2573 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002574 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002575 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002576 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002577 if worker_build:
2578 if worker_build.isWaiting():
2579 continue
2580 else:
2581 self.log.debug("%s is running" % worker_build)
2582 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002583 else:
James E. Blair962220f2016-08-03 11:22:38 -07002584 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002585 return False
James E. Blaira002b032017-04-18 10:35:48 -07002586 for (build_uuid, job_worker) in \
2587 self.executor_server.job_workers.items():
2588 if build_uuid not in seen_builds:
2589 self.log.debug("%s is not finalized" % build_uuid)
2590 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002591 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002592
James E. Blairdce6cea2016-12-20 16:45:32 -08002593 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002594 if self.fake_nodepool.paused:
2595 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002596 if self.sched.nodepool.requests:
2597 return False
2598 return True
2599
James E. Blaira615c362017-10-02 17:34:42 -07002600 def areAllMergeJobsWaiting(self):
2601 for client_job in list(self.merge_client.jobs):
2602 if not client_job.handle:
2603 self.log.debug("%s has no handle" % client_job)
2604 return False
2605 server_job = self.gearman_server.jobs.get(client_job.handle)
2606 if not server_job:
2607 self.log.debug("%s is not known to the gearman server" %
2608 client_job)
2609 return False
2610 if not hasattr(server_job, 'waiting'):
2611 self.log.debug("%s is being enqueued" % server_job)
2612 return False
2613 if server_job.waiting:
2614 self.log.debug("%s is waiting" % server_job)
2615 continue
2616 self.log.debug("%s is not waiting" % server_job)
2617 return False
2618 return True
2619
Jan Hruban6b71aff2015-10-22 16:58:08 +02002620 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002621 for event_queue in self.event_queues:
2622 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002623
2624 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002625 for event_queue in self.event_queues:
2626 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002627
Clark Boylanb640e052014-04-03 16:41:46 -07002628 def waitUntilSettled(self):
2629 self.log.debug("Waiting until settled...")
2630 start = time.time()
2631 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002632 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002633 self.log.error("Timeout waiting for Zuul to settle")
2634 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002635 for event_queue in self.event_queues:
2636 self.log.error(" %s: %s" %
2637 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002638 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002639 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002640 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002641 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002642 self.log.error("All requests completed: %s" %
2643 (self.areAllNodeRequestsComplete(),))
2644 self.log.error("Merge client jobs: %s" %
2645 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002646 raise Exception("Timeout waiting for Zuul to settle")
2647 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002648
Paul Belanger174a8272017-03-14 13:20:10 -04002649 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002650 # have all build states propogated to zuul?
2651 if self.haveAllBuildsReported():
2652 # Join ensures that the queue is empty _and_ events have been
2653 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002654 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002655 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002656 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002657 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002658 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002659 self.areAllNodeRequestsComplete() and
2660 all(self.eventQueuesEmpty())):
2661 # The queue empty check is placed at the end to
2662 # ensure that if a component adds an event between
2663 # when locked the run handler and checked that the
2664 # components were stable, we don't erroneously
2665 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002666 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002667 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002668 self.log.debug("...settled.")
2669 return
2670 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002671 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002672 self.sched.wake_event.wait(0.1)
2673
2674 def countJobResults(self, jobs, result):
2675 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002676 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002677
Monty Taylor0d926122017-05-24 08:07:56 -05002678 def getBuildByName(self, name):
2679 for build in self.builds:
2680 if build.name == name:
2681 return build
2682 raise Exception("Unable to find build %s" % name)
2683
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002684 def assertJobNotInHistory(self, name, project=None):
2685 for job in self.history:
2686 if (project is None or
2687 job.parameters['zuul']['project']['name'] == project):
2688 self.assertNotEqual(job.name, name,
2689 'Job %s found in history' % name)
2690
James E. Blair96c6bf82016-01-15 16:20:40 -08002691 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002692 for job in self.history:
2693 if (job.name == name and
2694 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002695 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002696 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002697 raise Exception("Unable to find job %s in history" % name)
2698
2699 def assertEmptyQueues(self):
2700 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002701 for tenant in self.sched.abide.tenants.values():
2702 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002703 for pipeline_queue in pipeline.queues:
2704 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002705 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002706 pipeline.name, pipeline_queue.name,
2707 pipeline_queue.queue))
2708 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002709 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002710
2711 def assertReportedStat(self, key, value=None, kind=None):
2712 start = time.time()
2713 while time.time() < (start + 5):
2714 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002715 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002716 if key == k:
2717 if value is None and kind is None:
2718 return
2719 elif value:
2720 if value == v:
2721 return
2722 elif kind:
2723 if v.endswith('|' + kind):
2724 return
2725 time.sleep(0.1)
2726
Clark Boylanb640e052014-04-03 16:41:46 -07002727 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002728
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002729 def assertBuilds(self, builds):
2730 """Assert that the running builds are as described.
2731
2732 The list of running builds is examined and must match exactly
2733 the list of builds described by the input.
2734
2735 :arg list builds: A list of dictionaries. Each item in the
2736 list must match the corresponding build in the build
2737 history, and each element of the dictionary must match the
2738 corresponding attribute of the build.
2739
2740 """
James E. Blair3158e282016-08-19 09:34:11 -07002741 try:
2742 self.assertEqual(len(self.builds), len(builds))
2743 for i, d in enumerate(builds):
2744 for k, v in d.items():
2745 self.assertEqual(
2746 getattr(self.builds[i], k), v,
2747 "Element %i in builds does not match" % (i,))
2748 except Exception:
2749 for build in self.builds:
2750 self.log.error("Running build: %s" % build)
2751 else:
2752 self.log.error("No running builds")
2753 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002754
James E. Blairb536ecc2016-08-31 10:11:42 -07002755 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002756 """Assert that the completed builds are as described.
2757
2758 The list of completed builds is examined and must match
2759 exactly the list of builds described by the input.
2760
2761 :arg list history: A list of dictionaries. Each item in the
2762 list must match the corresponding build in the build
2763 history, and each element of the dictionary must match the
2764 corresponding attribute of the build.
2765
James E. Blairb536ecc2016-08-31 10:11:42 -07002766 :arg bool ordered: If true, the history must match the order
2767 supplied, if false, the builds are permitted to have
2768 arrived in any order.
2769
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002770 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002771 def matches(history_item, item):
2772 for k, v in item.items():
2773 if getattr(history_item, k) != v:
2774 return False
2775 return True
James E. Blair3158e282016-08-19 09:34:11 -07002776 try:
2777 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002778 if ordered:
2779 for i, d in enumerate(history):
2780 if not matches(self.history[i], d):
2781 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002782 "Element %i in history does not match %s" %
2783 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002784 else:
2785 unseen = self.history[:]
2786 for i, d in enumerate(history):
2787 found = False
2788 for unseen_item in unseen:
2789 if matches(unseen_item, d):
2790 found = True
2791 unseen.remove(unseen_item)
2792 break
2793 if not found:
2794 raise Exception("No match found for element %i "
2795 "in history" % (i,))
2796 if unseen:
2797 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002798 except Exception:
2799 for build in self.history:
2800 self.log.error("Completed build: %s" % build)
2801 else:
2802 self.log.error("No completed builds")
2803 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002804
James E. Blair6ac368c2016-12-22 18:07:20 -08002805 def printHistory(self):
2806 """Log the build history.
2807
2808 This can be useful during tests to summarize what jobs have
2809 completed.
2810
2811 """
2812 self.log.debug("Build history:")
2813 for build in self.history:
2814 self.log.debug(build)
2815
James E. Blair59fdbac2015-12-07 17:08:06 -08002816 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002817 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2818
James E. Blair9ea70072017-04-19 16:05:30 -07002819 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002820 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002821 if not os.path.exists(root):
2822 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002823 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2824 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002825- tenant:
2826 name: openstack
2827 source:
2828 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002829 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002830 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002831 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002832 - org/project
2833 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002834 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002835 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002836 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002837 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002838 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002839
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002840 def addTagToRepo(self, project, name, sha):
2841 path = os.path.join(self.upstream_root, project)
2842 repo = git.Repo(path)
2843 repo.git.tag(name, sha)
2844
2845 def delTagFromRepo(self, project, name):
2846 path = os.path.join(self.upstream_root, project)
2847 repo = git.Repo(path)
2848 repo.git.tag('-d', name)
2849
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002850 def addCommitToRepo(self, project, message, files,
2851 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002852 path = os.path.join(self.upstream_root, project)
2853 repo = git.Repo(path)
2854 repo.head.reference = branch
2855 zuul.merger.merger.reset_repo_to_head(repo)
2856 for fn, content in files.items():
2857 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002858 try:
2859 os.makedirs(os.path.dirname(fn))
2860 except OSError:
2861 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002862 with open(fn, 'w') as f:
2863 f.write(content)
2864 repo.index.add([fn])
2865 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002866 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002867 repo.heads[branch].commit = commit
2868 repo.head.reference = branch
2869 repo.git.clean('-x', '-f', '-d')
2870 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002871 if tag:
2872 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002873 return before
2874
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002875 def commitConfigUpdate(self, project_name, source_name):
2876 """Commit an update to zuul.yaml
2877
2878 This overwrites the zuul.yaml in the specificed project with
2879 the contents specified.
2880
2881 :arg str project_name: The name of the project containing
2882 zuul.yaml (e.g., common-config)
2883
2884 :arg str source_name: The path to the file (underneath the
2885 test fixture directory) whose contents should be used to
2886 replace zuul.yaml.
2887 """
2888
2889 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002890 files = {}
2891 with open(source_path, 'r') as f:
2892 data = f.read()
2893 layout = yaml.safe_load(data)
2894 files['zuul.yaml'] = data
2895 for item in layout:
2896 if 'job' in item:
2897 jobname = item['job']['name']
2898 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002899 before = self.addCommitToRepo(
2900 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002901 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002902 return before
2903
Clint Byrum627ba362017-08-14 13:20:40 -07002904 def newTenantConfig(self, source_name):
2905 """ Use this to update the tenant config file in tests
2906
2907 This will update self.tenant_config_file to point to a temporary file
2908 for the duration of this particular test. The content of that file will
2909 be taken from FIXTURE_DIR/source_name
2910
2911 After the test the original value of self.tenant_config_file will be
2912 restored.
2913
2914 :arg str source_name: The path of the file under
2915 FIXTURE_DIR that will be used to populate the new tenant
2916 config file.
2917 """
2918 source_path = os.path.join(FIXTURE_DIR, source_name)
2919 orig_tenant_config_file = self.tenant_config_file
2920 with tempfile.NamedTemporaryFile(
2921 delete=False, mode='wb') as new_tenant_config:
2922 self.tenant_config_file = new_tenant_config.name
2923 with open(source_path, mode='rb') as source_tenant_config:
2924 new_tenant_config.write(source_tenant_config.read())
2925 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2926 self.setupAllProjectKeys()
2927 self.log.debug(
2928 'tenant_config_file = {}'.format(self.tenant_config_file))
2929
2930 def _restoreTenantConfig():
2931 self.log.debug(
2932 'restoring tenant_config_file = {}'.format(
2933 orig_tenant_config_file))
2934 os.unlink(self.tenant_config_file)
2935 self.tenant_config_file = orig_tenant_config_file
2936 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2937 self.addCleanup(_restoreTenantConfig)
2938
James E. Blair7fc8daa2016-08-08 15:37:15 -07002939 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002940
James E. Blair7fc8daa2016-08-08 15:37:15 -07002941 """Inject a Fake (Gerrit) event.
2942
2943 This method accepts a JSON-encoded event and simulates Zuul
2944 having received it from Gerrit. It could (and should)
2945 eventually apply to any connection type, but is currently only
2946 used with Gerrit connections. The name of the connection is
2947 used to look up the corresponding server, and the event is
2948 simulated as having been received by all Zuul connections
2949 attached to that server. So if two Gerrit connections in Zuul
2950 are connected to the same Gerrit server, and you invoke this
2951 method specifying the name of one of them, the event will be
2952 received by both.
2953
2954 .. note::
2955
2956 "self.fake_gerrit.addEvent" calls should be migrated to
2957 this method.
2958
2959 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002960 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002961 :arg str event: The JSON-encoded event.
2962
2963 """
2964 specified_conn = self.connections.connections[connection]
2965 for conn in self.connections.connections.values():
2966 if (isinstance(conn, specified_conn.__class__) and
2967 specified_conn.server == conn.server):
2968 conn.addEvent(event)
2969
James E. Blaird8af5422017-05-24 13:59:40 -07002970 def getUpstreamRepos(self, projects):
2971 """Return upstream git repo objects for the listed projects
2972
2973 :arg list projects: A list of strings, each the canonical name
2974 of a project.
2975
2976 :returns: A dictionary of {name: repo} for every listed
2977 project.
2978 :rtype: dict
2979
2980 """
2981
2982 repos = {}
2983 for project in projects:
2984 # FIXME(jeblair): the upstream root does not yet have a
2985 # hostname component; that needs to be added, and this
2986 # line removed:
2987 tmp_project_name = '/'.join(project.split('/')[1:])
2988 path = os.path.join(self.upstream_root, tmp_project_name)
2989 repo = git.Repo(path)
2990 repos[project] = repo
2991 return repos
2992
James E. Blair3f876d52016-07-22 13:07:14 -07002993
2994class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002995 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002996 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002997
Jamie Lennox7655b552017-03-17 12:33:38 +11002998 @contextmanager
2999 def jobLog(self, build):
3000 """Print job logs on assertion errors
3001
3002 This method is a context manager which, if it encounters an
3003 ecxeption, adds the build log to the debug output.
3004
3005 :arg Build build: The build that's being asserted.
3006 """
3007 try:
3008 yield
3009 except Exception:
3010 path = os.path.join(self.test_root, build.uuid,
3011 'work', 'logs', 'job-output.txt')
3012 with open(path) as f:
3013 self.log.debug(f.read())
3014 raise
3015
Joshua Heskethd78b4482015-09-14 16:56:34 -06003016
Paul Belanger0a21f0a2017-06-13 13:14:42 -04003017class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04003018 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04003019 use_ssl = True
3020
3021
Joshua Heskethd78b4482015-09-14 16:56:34 -06003022class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08003023 def setup_config(self):
3024 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06003025 for section_name in self.config.sections():
3026 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
3027 section_name, re.I)
3028 if not con_match:
3029 continue
3030
3031 if self.config.get(section_name, 'driver') == 'sql':
3032 f = MySQLSchemaFixture()
3033 self.useFixture(f)
3034 if (self.config.get(section_name, 'dburi') ==
3035 '$MYSQL_FIXTURE_DBURI$'):
3036 self.config.set(section_name, 'dburi', f.dburi)