blob: e688abd023ec0919010d93ab5c465d6cbea45c74 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Jamie Lennox7655b552017-03-17 12:33:38 +110019from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080020import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070021import gc
22import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050023from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043import git
44import gear
45import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080046import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080047import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060048import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070049import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blair6bacffb2018-01-05 13:45:25 -080055import tests.fakegithub
James E. Blaire511d2f2016-12-08 15:22:26 -080056import zuul.driver.gerrit.gerritsource as gerritsource
57import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070058import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.scheduler
60import zuul.webapp
Paul Belanger174a8272017-03-14 13:20:10 -040061import zuul.executor.server
62import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080063import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070065import zuul.merger.merger
66import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020067import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070068import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080069import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070070import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010071from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070072
73FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
74 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080075
76KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070077
Clark Boylanb640e052014-04-03 16:41:46 -070078
79def repack_repo(path):
80 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
81 output = subprocess.Popen(cmd, close_fds=True,
82 stdout=subprocess.PIPE,
83 stderr=subprocess.PIPE)
84 out = output.communicate()
85 if output.returncode:
86 raise Exception("git repack returned %d" % output.returncode)
87 return out
88
89
90def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040091 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070092
93
James E. Blaira190f3b2015-01-05 14:56:54 -080094def iterate_timeout(max_seconds, purpose):
95 start = time.time()
96 count = 0
97 while (time.time() < start + max_seconds):
98 count += 1
99 yield count
100 time.sleep(0)
101 raise Exception("Timeout waiting for %s" % purpose)
102
103
Jesse Keating436a5452017-04-20 11:48:41 -0700104def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700105 """Specify a layout file for use by a test method.
106
107 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700108 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700109
110 Some tests require only a very simple configuration. For those,
111 establishing a complete config directory hierachy is too much
112 work. In those cases, you can add a simple zuul.yaml file to the
113 test fixtures directory (in fixtures/layouts/foo.yaml) and use
114 this decorator to indicate the test method should use that rather
115 than the tenant config file specified by the test class.
116
117 The decorator will cause that layout file to be added to a
118 config-project called "common-config" and each "project" instance
119 referenced in the layout file will have a git repo automatically
120 initialized.
121 """
122
123 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700124 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700125 return test
126 return decorator
127
128
Gregory Haynes4fc12542015-04-22 20:38:06 -0700129class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700130 _common_path_default = "refs/changes"
131 _points_to_commits_only = True
132
133
Gregory Haynes4fc12542015-04-22 20:38:06 -0700134class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200135 categories = {'Approved': ('Approved', -1, 1),
136 'Code-Review': ('Code-Review', -2, 2),
137 'Verified': ('Verified', -2, 2)}
138
Clark Boylanb640e052014-04-03 16:41:46 -0700139 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700140 status='NEW', upstream_root=None, files={},
141 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
James E. Blair0e4c7912018-01-02 14:20:17 -0800173 'url': 'https://%s/%s' % (self.gerrit.server, number)}
Clark Boylanb640e052014-04-03 16:41:46 -0700174
175 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700176 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair289f5932017-07-27 15:02:29 -0700180 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700183 if parent is None:
184 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700185 ref = GerritChangeReference.create(
186 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700187 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700189 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.git.clean('-x', '-f', '-d')
191
192 path = os.path.join(self.upstream_root, self.project)
193 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for fn, content in files.items():
195 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700196 if content is None:
197 os.unlink(fn)
198 repo.index.remove([fn])
199 else:
200 d = os.path.dirname(fn)
201 if not os.path.exists(d):
202 os.makedirs(d)
203 with open(fn, 'w') as f:
204 f.write(content)
205 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700206 else:
207 for fni in range(100):
208 fn = os.path.join(path, str(fni))
209 f = open(fn, 'w')
210 for ci in range(4096):
211 f.write(random.choice(string.printable))
212 f.close()
213 repo.index.add([fn])
214
215 r = repo.index.commit(msg)
216 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700217 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700218 repo.git.clean('-x', '-f', '-d')
219 repo.heads['master'].checkout()
220 return r
221
James E. Blair289f5932017-07-27 15:02:29 -0700222 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700223 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700225 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 data = ("test %s %s %s\n" %
227 (self.branch, self.number, self.latest_patchset))
228 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700229 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700230 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700231 ps_files = [{'file': '/COMMIT_MSG',
232 'type': 'ADDED'},
233 {'file': 'README',
234 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700235 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700236 ps_files.append({'file': f, 'type': 'ADDED'})
237 d = {'approvals': [],
238 'createdOn': time.time(),
239 'files': ps_files,
240 'number': str(self.latest_patchset),
241 'ref': 'refs/changes/1/%s/%s' % (self.number,
242 self.latest_patchset),
243 'revision': c.hexsha,
244 'uploader': {'email': 'user@example.com',
245 'name': 'User name',
246 'username': 'user'}}
247 self.data['currentPatchSet'] = d
248 self.patchsets.append(d)
249 self.data['submitRecords'] = self.getSubmitRecords()
250
251 def getPatchsetCreatedEvent(self, patchset):
252 event = {"type": "patchset-created",
253 "change": {"project": self.project,
254 "branch": self.branch,
255 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
256 "number": str(self.number),
257 "subject": self.subject,
258 "owner": {"name": "User Name"},
259 "url": "https://hostname/3"},
260 "patchSet": self.patchsets[patchset - 1],
261 "uploader": {"name": "User Name"}}
262 return event
263
264 def getChangeRestoredEvent(self):
265 event = {"type": "change-restored",
266 "change": {"project": self.project,
267 "branch": self.branch,
268 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
269 "number": str(self.number),
270 "subject": self.subject,
271 "owner": {"name": "User Name"},
272 "url": "https://hostname/3"},
273 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100274 "patchSet": self.patchsets[-1],
275 "reason": ""}
276 return event
277
278 def getChangeAbandonedEvent(self):
279 event = {"type": "change-abandoned",
280 "change": {"project": self.project,
281 "branch": self.branch,
282 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
283 "number": str(self.number),
284 "subject": self.subject,
285 "owner": {"name": "User Name"},
286 "url": "https://hostname/3"},
287 "abandoner": {"name": "User Name"},
288 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700289 "reason": ""}
290 return event
291
292 def getChangeCommentEvent(self, patchset):
293 event = {"type": "comment-added",
294 "change": {"project": self.project,
295 "branch": self.branch,
296 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
297 "number": str(self.number),
298 "subject": self.subject,
299 "owner": {"name": "User Name"},
300 "url": "https://hostname/3"},
301 "patchSet": self.patchsets[patchset - 1],
302 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200303 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700304 "description": "Code-Review",
305 "value": "0"}],
306 "comment": "This is a comment"}
307 return event
308
James E. Blairc2a5ed72017-02-20 14:12:01 -0500309 def getChangeMergedEvent(self):
310 event = {"submitter": {"name": "Jenkins",
311 "username": "jenkins"},
312 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
313 "patchSet": self.patchsets[-1],
314 "change": self.data,
315 "type": "change-merged",
316 "eventCreatedOn": 1487613810}
317 return event
318
James E. Blair8cce42e2016-10-18 08:18:36 -0700319 def getRefUpdatedEvent(self):
320 path = os.path.join(self.upstream_root, self.project)
321 repo = git.Repo(path)
322 oldrev = repo.heads[self.branch].commit.hexsha
323
324 event = {
325 "type": "ref-updated",
326 "submitter": {
327 "name": "User Name",
328 },
329 "refUpdate": {
330 "oldRev": oldrev,
331 "newRev": self.patchsets[-1]['revision'],
332 "refName": self.branch,
333 "project": self.project,
334 }
335 }
336 return event
337
Joshua Hesketh642824b2014-07-01 17:54:59 +1000338 def addApproval(self, category, value, username='reviewer_john',
339 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700340 if not granted_on:
341 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000342 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200343 'description': self.categories[category][0],
344 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000345 'value': str(value),
346 'by': {
347 'username': username,
348 'email': username + '@example.com',
349 },
350 'grantedOn': int(granted_on)
351 }
Clark Boylanb640e052014-04-03 16:41:46 -0700352 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200353 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700354 del self.patchsets[-1]['approvals'][i]
355 self.patchsets[-1]['approvals'].append(approval)
356 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000357 'author': {'email': 'author@example.com',
358 'name': 'Patchset Author',
359 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'change': {'branch': self.branch,
361 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
362 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000363 'owner': {'email': 'owner@example.com',
364 'name': 'Change Owner',
365 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700366 'project': self.project,
367 'subject': self.subject,
368 'topic': 'master',
369 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000370 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700371 'patchSet': self.patchsets[-1],
372 'type': 'comment-added'}
373 self.data['submitRecords'] = self.getSubmitRecords()
374 return json.loads(json.dumps(event))
375
376 def getSubmitRecords(self):
377 status = {}
378 for cat in self.categories.keys():
379 status[cat] = 0
380
381 for a in self.patchsets[-1]['approvals']:
382 cur = status[a['type']]
383 cat_min, cat_max = self.categories[a['type']][1:]
384 new = int(a['value'])
385 if new == cat_min:
386 cur = new
387 elif abs(new) > abs(cur):
388 cur = new
389 status[a['type']] = cur
390
391 labels = []
392 ok = True
393 for typ, cat in self.categories.items():
394 cur = status[typ]
395 cat_min, cat_max = cat[1:]
396 if cur == cat_min:
397 value = 'REJECT'
398 ok = False
399 elif cur == cat_max:
400 value = 'OK'
401 else:
402 value = 'NEED'
403 ok = False
404 labels.append({'label': cat[0], 'status': value})
405 if ok:
406 return [{'status': 'OK'}]
407 return [{'status': 'NOT_READY',
408 'labels': labels}]
409
410 def setDependsOn(self, other, patchset):
411 self.depends_on_change = other
412 d = {'id': other.data['id'],
413 'number': other.data['number'],
414 'ref': other.patchsets[patchset - 1]['ref']
415 }
416 self.data['dependsOn'] = [d]
417
418 other.needed_by_changes.append(self)
419 needed = other.data.get('neededBy', [])
420 d = {'id': self.data['id'],
421 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700422 'ref': self.patchsets[-1]['ref'],
423 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700424 }
425 needed.append(d)
426 other.data['neededBy'] = needed
427
428 def query(self):
429 self.queried += 1
430 d = self.data.get('dependsOn')
431 if d:
432 d = d[0]
433 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
434 d['isCurrentPatchSet'] = True
435 else:
436 d['isCurrentPatchSet'] = False
437 return json.loads(json.dumps(self.data))
438
439 def setMerged(self):
440 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000441 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700442 return
443 if self.fail_merge:
444 return
445 self.data['status'] = 'MERGED'
446 self.open = False
447
448 path = os.path.join(self.upstream_root, self.project)
449 repo = git.Repo(path)
450 repo.heads[self.branch].commit = \
451 repo.commit(self.patchsets[-1]['revision'])
452
453 def setReported(self):
454 self.reported += 1
455
456
James E. Blaire511d2f2016-12-08 15:22:26 -0800457class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700458 """A Fake Gerrit connection for use in tests.
459
460 This subclasses
461 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
462 ability for tests to add changes to the fake Gerrit it represents.
463 """
464
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700466
James E. Blaire511d2f2016-12-08 15:22:26 -0800467 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700468 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800469 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 connection_config)
471
Monty Taylorb934c1a2017-06-16 19:31:47 -0500472 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
474 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000475 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700476 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200477 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700478
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700479 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700480 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700481 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700482 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700483 c = FakeGerritChange(self, self.change_number, project, branch,
484 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700485 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700486 self.changes[self.change_number] = c
487 return c
488
James E. Blair1edfd972017-12-01 15:54:24 -0800489 def addFakeTag(self, project, branch, tag):
490 path = os.path.join(self.upstream_root, project)
491 repo = git.Repo(path)
492 commit = repo.heads[branch].commit
493 newrev = commit.hexsha
494 ref = 'refs/tags/' + tag
495
496 git.Tag.create(repo, tag, commit)
497
498 event = {
499 "type": "ref-updated",
500 "submitter": {
501 "name": "User Name",
502 },
503 "refUpdate": {
504 "oldRev": 40 * '0',
505 "newRev": newrev,
506 "refName": ref,
507 "project": project,
508 }
509 }
510 return event
511
James E. Blair72facdc2017-08-17 10:29:12 -0700512 def getFakeBranchCreatedEvent(self, project, branch):
513 path = os.path.join(self.upstream_root, project)
514 repo = git.Repo(path)
515 oldrev = 40 * '0'
516
517 event = {
518 "type": "ref-updated",
519 "submitter": {
520 "name": "User Name",
521 },
522 "refUpdate": {
523 "oldRev": oldrev,
524 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700525 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700526 "project": project,
527 }
528 }
529 return event
530
Clark Boylanb640e052014-04-03 16:41:46 -0700531 def review(self, project, changeid, message, action):
532 number, ps = changeid.split(',')
533 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000534
535 # Add the approval back onto the change (ie simulate what gerrit would
536 # do).
537 # Usually when zuul leaves a review it'll create a feedback loop where
538 # zuul's review enters another gerrit event (which is then picked up by
539 # zuul). However, we can't mimic this behaviour (by adding this
540 # approval event into the queue) as it stops jobs from checking what
541 # happens before this event is triggered. If a job needs to see what
542 # happens they can add their own verified event into the queue.
543 # Nevertheless, we can update change with the new review in gerrit.
544
James E. Blair8b5408c2016-08-08 15:37:46 -0700545 for cat in action.keys():
546 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000547 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000548
Clark Boylanb640e052014-04-03 16:41:46 -0700549 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000550
Clark Boylanb640e052014-04-03 16:41:46 -0700551 if 'submit' in action:
552 change.setMerged()
553 if message:
554 change.setReported()
555
556 def query(self, number):
557 change = self.changes.get(int(number))
558 if change:
559 return change.query()
560 return {}
561
James E. Blair0e4c7912018-01-02 14:20:17 -0800562 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800563 if query.startswith('change:'):
564 # Query a specific changeid
565 changeid = query[len('change:'):]
566 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800567 if (change.data['id'] == changeid or
568 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700569 elif query.startswith('message:'):
570 # Query the content of a commit message
571 msg = query[len('message:'):].strip()
572 l = [change.query() for change in self.changes.values()
573 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800574 else:
575 # Query all open changes
576 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700577 return l
James E. Blairc494d542014-08-06 09:23:52 -0700578
James E. Blair0e4c7912018-01-02 14:20:17 -0800579 def simpleQuery(self, query):
580 self.log.debug("simpleQuery: %s" % query)
581 self.queries.append(query)
582 results = []
583 if query.startswith('(') and 'OR' in query:
584 query = query[1:-2]
585 for q in query.split(' OR '):
586 for r in self._simpleQuery(q):
587 if r not in results:
588 results.append(r)
589 else:
590 results = self._simpleQuery(query)
591 return results
592
Joshua Hesketh352264b2015-08-11 23:42:08 +1000593 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700594 pass
595
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200596 def _uploadPack(self, project):
597 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
598 'multi_ack thin-pack side-band side-band-64k ofs-delta '
599 'shallow no-progress include-tag multi_ack_detailed no-done\n')
600 path = os.path.join(self.upstream_root, project.name)
601 repo = git.Repo(path)
602 for ref in repo.refs:
603 r = ref.object.hexsha + ' ' + ref.path + '\n'
604 ret += '%04x%s' % (len(r) + 4, r)
605 ret += '0000'
606 return ret
607
Joshua Hesketh352264b2015-08-11 23:42:08 +1000608 def getGitUrl(self, project):
609 return os.path.join(self.upstream_root, project.name)
610
Clark Boylanb640e052014-04-03 16:41:46 -0700611
Gregory Haynes4fc12542015-04-22 20:38:06 -0700612class GithubChangeReference(git.Reference):
613 _common_path_default = "refs/pull"
614 _points_to_commits_only = True
615
616
617class FakeGithubPullRequest(object):
618
619 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800620 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700621 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700622 """Creates a new PR with several commits.
623 Sends an event about opened PR."""
624 self.github = github
625 self.source = github
626 self.number = number
627 self.project = project
628 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100629 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700630 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100631 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700632 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100633 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700634 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100635 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100636 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800637 self.reviews = []
638 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700639 self.updated_at = None
640 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100641 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100642 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700643 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700644 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100645 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700646 self._updateTimeStamp()
647
Jan Hruban570d01c2016-03-10 21:51:32 +0100648 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700649 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100650 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700651 self._updateTimeStamp()
652
Jan Hruban570d01c2016-03-10 21:51:32 +0100653 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700654 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100655 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700656 self._updateTimeStamp()
657
658 def getPullRequestOpenedEvent(self):
659 return self._getPullRequestEvent('opened')
660
661 def getPullRequestSynchronizeEvent(self):
662 return self._getPullRequestEvent('synchronize')
663
664 def getPullRequestReopenedEvent(self):
665 return self._getPullRequestEvent('reopened')
666
667 def getPullRequestClosedEvent(self):
668 return self._getPullRequestEvent('closed')
669
Jesse Keatinga41566f2017-06-14 18:17:51 -0700670 def getPullRequestEditedEvent(self):
671 return self._getPullRequestEvent('edited')
672
Gregory Haynes4fc12542015-04-22 20:38:06 -0700673 def addComment(self, message):
674 self.comments.append(message)
675 self._updateTimeStamp()
676
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200677 def getCommentAddedEvent(self, text):
678 name = 'issue_comment'
679 data = {
680 'action': 'created',
681 'issue': {
682 'number': self.number
683 },
684 'comment': {
685 'body': text
686 },
687 'repository': {
688 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100689 },
690 'sender': {
691 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200692 }
693 }
694 return (name, data)
695
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800696 def getReviewAddedEvent(self, review):
697 name = 'pull_request_review'
698 data = {
699 'action': 'submitted',
700 'pull_request': {
701 'number': self.number,
702 'title': self.subject,
703 'updated_at': self.updated_at,
704 'base': {
705 'ref': self.branch,
706 'repo': {
707 'full_name': self.project
708 }
709 },
710 'head': {
711 'sha': self.head_sha
712 }
713 },
714 'review': {
715 'state': review
716 },
717 'repository': {
718 'full_name': self.project
719 },
720 'sender': {
721 'login': 'ghuser'
722 }
723 }
724 return (name, data)
725
Jan Hruban16ad31f2015-11-07 14:39:07 +0100726 def addLabel(self, name):
727 if name not in self.labels:
728 self.labels.append(name)
729 self._updateTimeStamp()
730 return self._getLabelEvent(name)
731
732 def removeLabel(self, name):
733 if name in self.labels:
734 self.labels.remove(name)
735 self._updateTimeStamp()
736 return self._getUnlabelEvent(name)
737
738 def _getLabelEvent(self, label):
739 name = 'pull_request'
740 data = {
741 'action': 'labeled',
742 'pull_request': {
743 'number': self.number,
744 'updated_at': self.updated_at,
745 'base': {
746 'ref': self.branch,
747 'repo': {
748 'full_name': self.project
749 }
750 },
751 'head': {
752 'sha': self.head_sha
753 }
754 },
755 'label': {
756 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100757 },
758 'sender': {
759 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100760 }
761 }
762 return (name, data)
763
764 def _getUnlabelEvent(self, label):
765 name = 'pull_request'
766 data = {
767 'action': 'unlabeled',
768 'pull_request': {
769 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100770 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100771 'updated_at': self.updated_at,
772 'base': {
773 'ref': self.branch,
774 'repo': {
775 'full_name': self.project
776 }
777 },
778 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800779 'sha': self.head_sha,
780 'repo': {
781 'full_name': self.project
782 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100783 }
784 },
785 'label': {
786 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100787 },
788 'sender': {
789 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100790 }
791 }
792 return (name, data)
793
Jesse Keatinga41566f2017-06-14 18:17:51 -0700794 def editBody(self, body):
795 self.body = body
796 self._updateTimeStamp()
797
Gregory Haynes4fc12542015-04-22 20:38:06 -0700798 def _getRepo(self):
799 repo_path = os.path.join(self.upstream_root, self.project)
800 return git.Repo(repo_path)
801
802 def _createPRRef(self):
803 repo = self._getRepo()
804 GithubChangeReference.create(
805 repo, self._getPRReference(), 'refs/tags/init')
806
Jan Hruban570d01c2016-03-10 21:51:32 +0100807 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700808 repo = self._getRepo()
809 ref = repo.references[self._getPRReference()]
810 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100811 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700812 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100813 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814 repo.head.reference = ref
815 zuul.merger.merger.reset_repo_to_head(repo)
816 repo.git.clean('-x', '-f', '-d')
817
Jan Hruban570d01c2016-03-10 21:51:32 +0100818 if files:
819 fn = files[0]
820 self.files = files
821 else:
822 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
823 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100824 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700825 fn = os.path.join(repo.working_dir, fn)
826 f = open(fn, 'w')
827 with open(fn, 'w') as f:
828 f.write("test %s %s\n" %
829 (self.branch, self.number))
830 repo.index.add([fn])
831
832 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800833 # Create an empty set of statuses for the given sha,
834 # each sha on a PR may have a status set on it
835 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700836 repo.head.reference = 'master'
837 zuul.merger.merger.reset_repo_to_head(repo)
838 repo.git.clean('-x', '-f', '-d')
839 repo.heads['master'].checkout()
840
841 def _updateTimeStamp(self):
842 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
843
844 def getPRHeadSha(self):
845 repo = self._getRepo()
846 return repo.references[self._getPRReference()].commit.hexsha
847
Jesse Keatingae4cd272017-01-30 17:10:44 -0800848 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800849 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
850 # convert the timestamp to a str format that would be returned
851 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800852
Adam Gandelmand81dd762017-02-09 15:15:49 -0800853 if granted_on:
854 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
855 submitted_at = time.strftime(
856 gh_time_format, granted_on.timetuple())
857 else:
858 # github timestamps only down to the second, so we need to make
859 # sure reviews that tests add appear to be added over a period of
860 # time in the past and not all at once.
861 if not self.reviews:
862 # the first review happens 10 mins ago
863 offset = 600
864 else:
865 # subsequent reviews happen 1 minute closer to now
866 offset = 600 - (len(self.reviews) * 60)
867
868 granted_on = datetime.datetime.utcfromtimestamp(
869 time.time() - offset)
870 submitted_at = time.strftime(
871 gh_time_format, granted_on.timetuple())
872
Jesse Keatingae4cd272017-01-30 17:10:44 -0800873 self.reviews.append({
874 'state': state,
875 'user': {
876 'login': user,
877 'email': user + "@derp.com",
878 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800879 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800880 })
881
Gregory Haynes4fc12542015-04-22 20:38:06 -0700882 def _getPRReference(self):
883 return '%s/head' % self.number
884
885 def _getPullRequestEvent(self, action):
886 name = 'pull_request'
887 data = {
888 'action': action,
889 'number': self.number,
890 'pull_request': {
891 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100892 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700893 'updated_at': self.updated_at,
894 'base': {
895 'ref': self.branch,
896 'repo': {
897 'full_name': self.project
898 }
899 },
900 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800901 'sha': self.head_sha,
902 'repo': {
903 'full_name': self.project
904 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700905 },
906 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100907 },
908 'sender': {
909 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700910 }
911 }
912 return (name, data)
913
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800914 def getCommitStatusEvent(self, context, state='success', user='zuul'):
915 name = 'status'
916 data = {
917 'state': state,
918 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700919 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800920 'description': 'Test results for %s: %s' % (self.head_sha, state),
921 'target_url': 'http://zuul/%s' % self.head_sha,
922 'branches': [],
923 'context': context,
924 'sender': {
925 'login': user
926 }
927 }
928 return (name, data)
929
James E. Blair289f5932017-07-27 15:02:29 -0700930 def setMerged(self, commit_message):
931 self.is_merged = True
932 self.merge_message = commit_message
933
934 repo = self._getRepo()
935 repo.heads[self.branch].commit = repo.commit(self.head_sha)
936
Gregory Haynes4fc12542015-04-22 20:38:06 -0700937
938class FakeGithubConnection(githubconnection.GithubConnection):
939 log = logging.getLogger("zuul.test.FakeGithubConnection")
940
941 def __init__(self, driver, connection_name, connection_config,
James E. Blair6bacffb2018-01-05 13:45:25 -0800942 changes_db=None, upstream_root=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700943 super(FakeGithubConnection, self).__init__(driver, connection_name,
944 connection_config)
945 self.connection_name = connection_name
946 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800947 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700948 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700949 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100950 self.merge_failure = False
951 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100952 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800953 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Tobias Henkel64e37a02017-08-02 10:13:30 +0200954
955 def getGithubClient(self,
956 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600957 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200958 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700959
Jesse Keatinga41566f2017-06-14 18:17:51 -0700960 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700961 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700962 self.pr_number += 1
963 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100964 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700965 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800966 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700967 return pull_request
968
Jesse Keating71a47ff2017-06-06 11:36:43 -0700969 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
970 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700971 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700972 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700973 if not new_rev:
974 new_rev = random_sha1()
975 name = 'push'
976 data = {
977 'ref': ref,
978 'before': old_rev,
979 'after': new_rev,
980 'repository': {
981 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700982 },
983 'commits': [
984 {
985 'added': added_files,
986 'removed': removed_files,
987 'modified': modified_files
988 }
989 ]
Wayne1a78c612015-06-11 17:14:13 -0700990 }
991 return (name, data)
992
Gregory Haynes4fc12542015-04-22 20:38:06 -0700993 def emitEvent(self, event):
994 """Emulates sending the GitHub webhook event to the connection."""
995 port = self.webapp.server.socket.getsockname()[1]
996 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700997 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -0700998 secret = self.connection_config['webhook_token']
999 signature = githubconnection._sign_request(payload, secret)
1000 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001001 req = urllib.request.Request(
1002 'http://localhost:%s/connection/%s/payload'
1003 % (port, self.connection_name),
1004 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001005 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001006
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001007 def addProject(self, project):
1008 # use the original method here and additionally register it in the
1009 # fake github
1010 super(FakeGithubConnection, self).addProject(project)
1011 self.getGithubClient(project).addProject(project)
1012
Jesse Keating9021a012017-08-29 14:45:27 -07001013 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001014 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001015 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001016 if len(prs) > 1:
1017 raise Exception('Multiple pulls found with head sha: %s' % sha)
1018 pr = prs[0]
1019 return self.getPull(pr.project, pr.number)
1020
Jesse Keatingae4cd272017-01-30 17:10:44 -08001021 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001022 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001023 return pr.reviews
1024
Jesse Keatingae4cd272017-01-30 17:10:44 -08001025 def getRepoPermission(self, project, login):
1026 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001027 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001028 pr_owner, pr_project = pr.project.split('/')
1029 if (pr_owner == owner and proj == pr_project):
1030 if login in pr.writers:
1031 return 'write'
1032 else:
1033 return 'read'
1034
Gregory Haynes4fc12542015-04-22 20:38:06 -07001035 def getGitUrl(self, project):
1036 return os.path.join(self.upstream_root, str(project))
1037
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001038 def real_getGitUrl(self, project):
1039 return super(FakeGithubConnection, self).getGitUrl(project)
1040
Jan Hrubane252a732017-01-03 15:03:09 +01001041 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001042 # record that this got reported
1043 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001044 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001045 pull_request.addComment(message)
1046
Jan Hruban3b415922016-02-03 13:10:22 +01001047 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001048 # record that this got reported
1049 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001050 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001051 if self.merge_failure:
1052 raise Exception('Pull request was not merged')
1053 if self.merge_not_allowed_count > 0:
1054 self.merge_not_allowed_count -= 1
1055 raise MergeFailure('Merge was not successful due to mergeability'
1056 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001057 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001058
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001059 def setCommitStatus(self, project, sha, state, url='', description='',
1060 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001061 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001062 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001063 super(FakeGithubConnection, self).setCommitStatus(
1064 project, sha, state,
1065 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001066
Jan Hruban16ad31f2015-11-07 14:39:07 +01001067 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001068 # record that this got reported
1069 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001070 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001071 pull_request.addLabel(label)
1072
1073 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001074 # record that this got reported
1075 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001076 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001077 pull_request.removeLabel(label)
1078
Gregory Haynes4fc12542015-04-22 20:38:06 -07001079
Clark Boylanb640e052014-04-03 16:41:46 -07001080class BuildHistory(object):
1081 def __init__(self, **kw):
1082 self.__dict__.update(kw)
1083
1084 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001085 return ("<Completed build, result: %s name: %s uuid: %s "
1086 "changes: %s ref: %s>" %
1087 (self.result, self.name, self.uuid,
1088 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001089
1090
Clark Boylanb640e052014-04-03 16:41:46 -07001091class FakeStatsd(threading.Thread):
1092 def __init__(self):
1093 threading.Thread.__init__(self)
1094 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001095 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001096 self.sock.bind(('', 0))
1097 self.port = self.sock.getsockname()[1]
1098 self.wake_read, self.wake_write = os.pipe()
1099 self.stats = []
1100
1101 def run(self):
1102 while True:
1103 poll = select.poll()
1104 poll.register(self.sock, select.POLLIN)
1105 poll.register(self.wake_read, select.POLLIN)
1106 ret = poll.poll()
1107 for (fd, event) in ret:
1108 if fd == self.sock.fileno():
1109 data = self.sock.recvfrom(1024)
1110 if not data:
1111 return
1112 self.stats.append(data[0])
1113 if fd == self.wake_read:
1114 return
1115
1116 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001117 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001118
1119
James E. Blaire1767bc2016-08-02 10:00:27 -07001120class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001121 log = logging.getLogger("zuul.test")
1122
Paul Belanger174a8272017-03-14 13:20:10 -04001123 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001124 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001125 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001126 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001127 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001128 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001129 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001130 # TODOv3(jeblair): self.node is really "the label of the node
1131 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001132 # keep using it like this, or we may end up exposing more of
1133 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001134 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001135 self.node = None
1136 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001137 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001138 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001139 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001140 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001141 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001142 self.wait_condition = threading.Condition()
1143 self.waiting = False
1144 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001145 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001146 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001147 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001148 items = self.parameters['zuul']['items']
1149 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1150 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001151
James E. Blair3158e282016-08-19 09:34:11 -07001152 def __repr__(self):
1153 waiting = ''
1154 if self.waiting:
1155 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001156 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1157 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001158
Clark Boylanb640e052014-04-03 16:41:46 -07001159 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001160 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001161 self.wait_condition.acquire()
1162 self.wait_condition.notify()
1163 self.waiting = False
1164 self.log.debug("Build %s released" % self.unique)
1165 self.wait_condition.release()
1166
1167 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001168 """Return whether this build is being held.
1169
1170 :returns: Whether the build is being held.
1171 :rtype: bool
1172 """
1173
Clark Boylanb640e052014-04-03 16:41:46 -07001174 self.wait_condition.acquire()
1175 if self.waiting:
1176 ret = True
1177 else:
1178 ret = False
1179 self.wait_condition.release()
1180 return ret
1181
1182 def _wait(self):
1183 self.wait_condition.acquire()
1184 self.waiting = True
1185 self.log.debug("Build %s waiting" % self.unique)
1186 self.wait_condition.wait()
1187 self.wait_condition.release()
1188
1189 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001190 self.log.debug('Running build %s' % self.unique)
1191
Paul Belanger174a8272017-03-14 13:20:10 -04001192 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001193 self.log.debug('Holding build %s' % self.unique)
1194 self._wait()
1195 self.log.debug("Build %s continuing" % self.unique)
1196
James E. Blair412fba82017-01-26 15:00:50 -08001197 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001198 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001199 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001200 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001201 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001202 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001203 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001204
James E. Blaire1767bc2016-08-02 10:00:27 -07001205 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001206
James E. Blaira5dba232016-08-08 15:53:24 -07001207 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001208 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001209 for change in changes:
1210 if self.hasChanges(change):
1211 return True
1212 return False
1213
James E. Blaire7b99a02016-08-05 14:27:34 -07001214 def hasChanges(self, *changes):
1215 """Return whether this build has certain changes in its git repos.
1216
1217 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001218 are expected to be present (in order) in the git repository of
1219 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001220
1221 :returns: Whether the build has the indicated changes.
1222 :rtype: bool
1223
1224 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001225 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001226 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001227 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001228 try:
1229 repo = git.Repo(path)
1230 except NoSuchPathError as e:
1231 self.log.debug('%s' % e)
1232 return False
James E. Blair247cab72017-07-20 16:52:36 -07001233 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001234 commit_message = '%s-1' % change.subject
1235 self.log.debug("Checking if build %s has changes; commit_message "
1236 "%s; repo_messages %s" % (self, commit_message,
1237 repo_messages))
1238 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001239 self.log.debug(" messages do not match")
1240 return False
1241 self.log.debug(" OK")
1242 return True
1243
James E. Blaird8af5422017-05-24 13:59:40 -07001244 def getWorkspaceRepos(self, projects):
1245 """Return workspace git repo objects for the listed projects
1246
1247 :arg list projects: A list of strings, each the canonical name
1248 of a project.
1249
1250 :returns: A dictionary of {name: repo} for every listed
1251 project.
1252 :rtype: dict
1253
1254 """
1255
1256 repos = {}
1257 for project in projects:
1258 path = os.path.join(self.jobdir.src_root, project)
1259 repo = git.Repo(path)
1260 repos[project] = repo
1261 return repos
1262
Clark Boylanb640e052014-04-03 16:41:46 -07001263
James E. Blair107bb252017-10-13 15:53:16 -07001264class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1265 def doMergeChanges(self, merger, items, repo_state):
1266 # Get a merger in order to update the repos involved in this job.
1267 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1268 merger, items, repo_state)
1269 if not commit: # merge conflict
1270 self.recordResult('MERGER_FAILURE')
1271 return commit
1272
1273 def recordResult(self, result):
1274 build = self.executor_server.job_builds[self.job.unique]
1275 self.executor_server.lock.acquire()
1276 self.executor_server.build_history.append(
1277 BuildHistory(name=build.name, result=result, changes=build.changes,
1278 node=build.node, uuid=build.unique,
1279 ref=build.parameters['zuul']['ref'],
1280 parameters=build.parameters, jobdir=build.jobdir,
1281 pipeline=build.parameters['zuul']['pipeline'])
1282 )
1283 self.executor_server.running_builds.remove(build)
1284 del self.executor_server.job_builds[self.job.unique]
1285 self.executor_server.lock.release()
1286
1287 def runPlaybooks(self, args):
1288 build = self.executor_server.job_builds[self.job.unique]
1289 build.jobdir = self.jobdir
1290
1291 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1292 self.recordResult(result)
1293 return result
1294
James E. Blaira86aaf12017-10-15 20:59:50 -07001295 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001296 build = self.executor_server.job_builds[self.job.unique]
1297
1298 if self.executor_server._run_ansible:
1299 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001300 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001301 else:
1302 if playbook.path:
1303 result = build.run()
1304 else:
1305 result = (self.RESULT_NORMAL, 0)
1306 return result
1307
1308 def getHostList(self, args):
1309 self.log.debug("hostlist")
1310 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1311 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001312 if not host['host_vars'].get('ansible_connection'):
1313 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001314
1315 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001316 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001317 host_vars=dict(ansible_connection='local'),
1318 host_keys=[]))
1319 return hosts
1320
1321
Paul Belanger174a8272017-03-14 13:20:10 -04001322class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1323 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001324
Paul Belanger174a8272017-03-14 13:20:10 -04001325 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001326 they will report that they have started but then pause until
1327 released before reporting completion. This attribute may be
1328 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001329 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001330 be explicitly released.
1331
1332 """
James E. Blairfaf81982017-10-10 15:42:26 -07001333
1334 _job_class = RecordingAnsibleJob
1335
James E. Blairf5dbd002015-12-23 15:26:17 -08001336 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001337 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001338 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001339 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001340 self.hold_jobs_in_build = False
1341 self.lock = threading.Lock()
1342 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001343 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001344 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001345 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001346
James E. Blaira5dba232016-08-08 15:53:24 -07001347 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001348 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001349
1350 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001351 :arg Change change: The :py:class:`~tests.base.FakeChange`
1352 instance which should cause the job to fail. This job
1353 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001354
1355 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001356 l = self.fail_tests.get(name, [])
1357 l.append(change)
1358 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001359
James E. Blair962220f2016-08-03 11:22:38 -07001360 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001361 """Release a held build.
1362
1363 :arg str regex: A regular expression which, if supplied, will
1364 cause only builds with matching names to be released. If
1365 not supplied, all builds will be released.
1366
1367 """
James E. Blair962220f2016-08-03 11:22:38 -07001368 builds = self.running_builds[:]
1369 self.log.debug("Releasing build %s (%s)" % (regex,
1370 len(self.running_builds)))
1371 for build in builds:
1372 if not regex or re.match(regex, build.name):
1373 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001374 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001375 build.release()
1376 else:
1377 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001378 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001379 self.log.debug("Done releasing builds %s (%s)" %
1380 (regex, len(self.running_builds)))
1381
Paul Belanger174a8272017-03-14 13:20:10 -04001382 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001383 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001384 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001385 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001386 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001387 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001388 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001389 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001390 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001391
1392 def stopJob(self, job):
1393 self.log.debug("handle stop")
1394 parameters = json.loads(job.arguments)
1395 uuid = parameters['uuid']
1396 for build in self.running_builds:
1397 if build.unique == uuid:
1398 build.aborted = True
1399 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001400 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001401
James E. Blaira002b032017-04-18 10:35:48 -07001402 def stop(self):
1403 for build in self.running_builds:
1404 build.release()
1405 super(RecordingExecutorServer, self).stop()
1406
Joshua Hesketh50c21782016-10-13 21:34:14 +11001407
Clark Boylanb640e052014-04-03 16:41:46 -07001408class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001409 """A Gearman server for use in tests.
1410
1411 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1412 added to the queue but will not be distributed to workers
1413 until released. This attribute may be changed at any time and
1414 will take effect for subsequently enqueued jobs, but
1415 previously held jobs will still need to be explicitly
1416 released.
1417
1418 """
1419
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001420 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001421 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001422 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001423 if use_ssl:
1424 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1425 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1426 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1427 else:
1428 ssl_ca = None
1429 ssl_cert = None
1430 ssl_key = None
1431
1432 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1433 ssl_cert=ssl_cert,
1434 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001435
1436 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001437 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1438 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001439 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001440 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001441 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001442 elif job.name.startswith(b'merger:'):
1443 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001444 else:
1445 job.waiting = False
1446 if job.waiting:
1447 continue
1448 if job.name in connection.functions:
1449 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001450 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001451 connection.related_jobs[job.handle] = job
1452 job.worker_connection = connection
1453 job.running = True
1454 return job
1455 return None
1456
1457 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001458 """Release a held job.
1459
1460 :arg str regex: A regular expression which, if supplied, will
1461 cause only jobs with matching names to be released. If
1462 not supplied, all jobs will be released.
1463 """
Clark Boylanb640e052014-04-03 16:41:46 -07001464 released = False
1465 qlen = (len(self.high_queue) + len(self.normal_queue) +
1466 len(self.low_queue))
1467 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1468 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001469 match = False
1470 if job.name == b'executor:execute':
1471 parameters = json.loads(job.arguments.decode('utf8'))
1472 if not regex or re.match(regex, parameters.get('job')):
1473 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001474 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001475 if not regex:
1476 match = True
1477 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001478 self.log.debug("releasing queued job %s" %
1479 job.unique)
1480 job.waiting = False
1481 released = True
1482 else:
1483 self.log.debug("not releasing queued job %s" %
1484 job.unique)
1485 if released:
1486 self.wakeConnections()
1487 qlen = (len(self.high_queue) + len(self.normal_queue) +
1488 len(self.low_queue))
1489 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1490
1491
1492class FakeSMTP(object):
1493 log = logging.getLogger('zuul.FakeSMTP')
1494
1495 def __init__(self, messages, server, port):
1496 self.server = server
1497 self.port = port
1498 self.messages = messages
1499
1500 def sendmail(self, from_email, to_email, msg):
1501 self.log.info("Sending email from %s, to %s, with msg %s" % (
1502 from_email, to_email, msg))
1503
1504 headers = msg.split('\n\n', 1)[0]
1505 body = msg.split('\n\n', 1)[1]
1506
1507 self.messages.append(dict(
1508 from_email=from_email,
1509 to_email=to_email,
1510 msg=msg,
1511 headers=headers,
1512 body=body,
1513 ))
1514
1515 return True
1516
1517 def quit(self):
1518 return True
1519
1520
James E. Blairdce6cea2016-12-20 16:45:32 -08001521class FakeNodepool(object):
1522 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001523 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001524
1525 log = logging.getLogger("zuul.test.FakeNodepool")
1526
1527 def __init__(self, host, port, chroot):
1528 self.client = kazoo.client.KazooClient(
1529 hosts='%s:%s%s' % (host, port, chroot))
1530 self.client.start()
1531 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001532 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001533 self.thread = threading.Thread(target=self.run)
1534 self.thread.daemon = True
1535 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001536 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001537
1538 def stop(self):
1539 self._running = False
1540 self.thread.join()
1541 self.client.stop()
1542 self.client.close()
1543
1544 def run(self):
1545 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001546 try:
1547 self._run()
1548 except Exception:
1549 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001550 time.sleep(0.1)
1551
1552 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001553 if self.paused:
1554 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001555 for req in self.getNodeRequests():
1556 self.fulfillRequest(req)
1557
1558 def getNodeRequests(self):
1559 try:
1560 reqids = self.client.get_children(self.REQUEST_ROOT)
1561 except kazoo.exceptions.NoNodeError:
1562 return []
1563 reqs = []
1564 for oid in sorted(reqids):
1565 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001566 try:
1567 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001568 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001569 data['_oid'] = oid
1570 reqs.append(data)
1571 except kazoo.exceptions.NoNodeError:
1572 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001573 return reqs
1574
James E. Blaire18d4602017-01-05 11:17:28 -08001575 def getNodes(self):
1576 try:
1577 nodeids = self.client.get_children(self.NODE_ROOT)
1578 except kazoo.exceptions.NoNodeError:
1579 return []
1580 nodes = []
1581 for oid in sorted(nodeids):
1582 path = self.NODE_ROOT + '/' + oid
1583 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001584 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001585 data['_oid'] = oid
1586 try:
1587 lockfiles = self.client.get_children(path + '/lock')
1588 except kazoo.exceptions.NoNodeError:
1589 lockfiles = []
1590 if lockfiles:
1591 data['_lock'] = True
1592 else:
1593 data['_lock'] = False
1594 nodes.append(data)
1595 return nodes
1596
James E. Blaira38c28e2017-01-04 10:33:20 -08001597 def makeNode(self, request_id, node_type):
1598 now = time.time()
1599 path = '/nodepool/nodes/'
1600 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001601 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001602 provider='test-provider',
1603 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001604 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001605 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001606 public_ipv4='127.0.0.1',
1607 private_ipv4=None,
1608 public_ipv6=None,
1609 allocated_to=request_id,
1610 state='ready',
1611 state_time=now,
1612 created_time=now,
1613 updated_time=now,
1614 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001615 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001616 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001617 if 'fakeuser' in node_type:
1618 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001619 if 'windows' in node_type:
1620 data['connection_type'] = 'winrm'
1621
Clint Byrumf322fe22017-05-10 20:53:12 -07001622 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001623 path = self.client.create(path, data,
1624 makepath=True,
1625 sequence=True)
1626 nodeid = path.split("/")[-1]
1627 return nodeid
1628
James E. Blair6ab79e02017-01-06 10:10:17 -08001629 def addFailRequest(self, request):
1630 self.fail_requests.add(request['_oid'])
1631
James E. Blairdce6cea2016-12-20 16:45:32 -08001632 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001633 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001634 return
1635 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001636 oid = request['_oid']
1637 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001638
James E. Blair6ab79e02017-01-06 10:10:17 -08001639 if oid in self.fail_requests:
1640 request['state'] = 'failed'
1641 else:
1642 request['state'] = 'fulfilled'
1643 nodes = []
1644 for node in request['node_types']:
1645 nodeid = self.makeNode(oid, node)
1646 nodes.append(nodeid)
1647 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001648
James E. Blaira38c28e2017-01-04 10:33:20 -08001649 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001650 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001651 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001652 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001653 try:
1654 self.client.set(path, data)
1655 except kazoo.exceptions.NoNodeError:
1656 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001657
1658
James E. Blair498059b2016-12-20 13:50:13 -08001659class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001660 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001661 super(ChrootedKazooFixture, self).__init__()
1662
1663 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1664 if ':' in zk_host:
1665 host, port = zk_host.split(':')
1666 else:
1667 host = zk_host
1668 port = None
1669
1670 self.zookeeper_host = host
1671
1672 if not port:
1673 self.zookeeper_port = 2181
1674 else:
1675 self.zookeeper_port = int(port)
1676
Clark Boylan621ec9a2017-04-07 17:41:33 -07001677 self.test_id = test_id
1678
James E. Blair498059b2016-12-20 13:50:13 -08001679 def _setUp(self):
1680 # Make sure the test chroot paths do not conflict
1681 random_bits = ''.join(random.choice(string.ascii_lowercase +
1682 string.ascii_uppercase)
1683 for x in range(8))
1684
Clark Boylan621ec9a2017-04-07 17:41:33 -07001685 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001686 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1687
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001688 self.addCleanup(self._cleanup)
1689
James E. Blair498059b2016-12-20 13:50:13 -08001690 # Ensure the chroot path exists and clean up any pre-existing znodes.
1691 _tmp_client = kazoo.client.KazooClient(
1692 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1693 _tmp_client.start()
1694
1695 if _tmp_client.exists(self.zookeeper_chroot):
1696 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1697
1698 _tmp_client.ensure_path(self.zookeeper_chroot)
1699 _tmp_client.stop()
1700 _tmp_client.close()
1701
James E. Blair498059b2016-12-20 13:50:13 -08001702 def _cleanup(self):
1703 '''Remove the chroot path.'''
1704 # Need a non-chroot'ed client to remove the chroot path
1705 _tmp_client = kazoo.client.KazooClient(
1706 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1707 _tmp_client.start()
1708 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1709 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001710 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001711
1712
Joshua Heskethd78b4482015-09-14 16:56:34 -06001713class MySQLSchemaFixture(fixtures.Fixture):
1714 def setUp(self):
1715 super(MySQLSchemaFixture, self).setUp()
1716
1717 random_bits = ''.join(random.choice(string.ascii_lowercase +
1718 string.ascii_uppercase)
1719 for x in range(8))
1720 self.name = '%s_%s' % (random_bits, os.getpid())
1721 self.passwd = uuid.uuid4().hex
1722 db = pymysql.connect(host="localhost",
1723 user="openstack_citest",
1724 passwd="openstack_citest",
1725 db="openstack_citest")
1726 cur = db.cursor()
1727 cur.execute("create database %s" % self.name)
1728 cur.execute(
1729 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1730 (self.name, self.name, self.passwd))
1731 cur.execute("flush privileges")
1732
1733 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1734 self.passwd,
1735 self.name)
1736 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1737 self.addCleanup(self.cleanup)
1738
1739 def cleanup(self):
1740 db = pymysql.connect(host="localhost",
1741 user="openstack_citest",
1742 passwd="openstack_citest",
1743 db="openstack_citest")
1744 cur = db.cursor()
1745 cur.execute("drop database %s" % self.name)
1746 cur.execute("drop user '%s'@'localhost'" % self.name)
1747 cur.execute("flush privileges")
1748
1749
Maru Newby3fe5f852015-01-13 04:22:14 +00001750class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001751 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001752 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001753
James E. Blair1c236df2017-02-01 14:07:24 -08001754 def attachLogs(self, *args):
1755 def reader():
1756 self._log_stream.seek(0)
1757 while True:
1758 x = self._log_stream.read(4096)
1759 if not x:
1760 break
1761 yield x.encode('utf8')
1762 content = testtools.content.content_from_reader(
1763 reader,
1764 testtools.content_type.UTF8_TEXT,
1765 False)
1766 self.addDetail('logging', content)
1767
Clark Boylanb640e052014-04-03 16:41:46 -07001768 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001769 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001770 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1771 try:
1772 test_timeout = int(test_timeout)
1773 except ValueError:
1774 # If timeout value is invalid do not set a timeout.
1775 test_timeout = 0
1776 if test_timeout > 0:
1777 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1778
1779 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1780 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1781 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1782 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1783 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1784 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1785 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1786 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1787 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1788 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001789 self._log_stream = StringIO()
1790 self.addOnException(self.attachLogs)
1791 else:
1792 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001793
James E. Blair73b41772017-05-22 13:22:55 -07001794 # NOTE(jeblair): this is temporary extra debugging to try to
1795 # track down a possible leak.
1796 orig_git_repo_init = git.Repo.__init__
1797
1798 def git_repo_init(myself, *args, **kw):
1799 orig_git_repo_init(myself, *args, **kw)
1800 self.log.debug("Created git repo 0x%x %s" %
1801 (id(myself), repr(myself)))
1802
1803 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1804 git_repo_init))
1805
James E. Blair1c236df2017-02-01 14:07:24 -08001806 handler = logging.StreamHandler(self._log_stream)
1807 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1808 '%(levelname)-8s %(message)s')
1809 handler.setFormatter(formatter)
1810
1811 logger = logging.getLogger()
1812 logger.setLevel(logging.DEBUG)
1813 logger.addHandler(handler)
1814
Clark Boylan3410d532017-04-25 12:35:29 -07001815 # Make sure we don't carry old handlers around in process state
1816 # which slows down test runs
1817 self.addCleanup(logger.removeHandler, handler)
1818 self.addCleanup(handler.close)
1819 self.addCleanup(handler.flush)
1820
James E. Blair1c236df2017-02-01 14:07:24 -08001821 # NOTE(notmorgan): Extract logging overrides for specific
1822 # libraries from the OS_LOG_DEFAULTS env and create loggers
1823 # for each. This is used to limit the output during test runs
1824 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001825 log_defaults_from_env = os.environ.get(
1826 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001827 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001828
James E. Blairdce6cea2016-12-20 16:45:32 -08001829 if log_defaults_from_env:
1830 for default in log_defaults_from_env.split(','):
1831 try:
1832 name, level_str = default.split('=', 1)
1833 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001834 logger = logging.getLogger(name)
1835 logger.setLevel(level)
1836 logger.addHandler(handler)
1837 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001838 except ValueError:
1839 # NOTE(notmorgan): Invalid format of the log default,
1840 # skip and don't try and apply a logger for the
1841 # specified module
1842 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001843
Maru Newby3fe5f852015-01-13 04:22:14 +00001844
1845class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001846 """A test case with a functioning Zuul.
1847
1848 The following class variables are used during test setup and can
1849 be overidden by subclasses but are effectively read-only once a
1850 test method starts running:
1851
1852 :cvar str config_file: This points to the main zuul config file
1853 within the fixtures directory. Subclasses may override this
1854 to obtain a different behavior.
1855
1856 :cvar str tenant_config_file: This is the tenant config file
1857 (which specifies from what git repos the configuration should
1858 be loaded). It defaults to the value specified in
1859 `config_file` but can be overidden by subclasses to obtain a
1860 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001861 configuration. See also the :py:func:`simple_layout`
1862 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001863
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001864 :cvar bool create_project_keys: Indicates whether Zuul should
1865 auto-generate keys for each project, or whether the test
1866 infrastructure should insert dummy keys to save time during
1867 startup. Defaults to False.
1868
James E. Blaire7b99a02016-08-05 14:27:34 -07001869 The following are instance variables that are useful within test
1870 methods:
1871
1872 :ivar FakeGerritConnection fake_<connection>:
1873 A :py:class:`~tests.base.FakeGerritConnection` will be
1874 instantiated for each connection present in the config file
1875 and stored here. For instance, `fake_gerrit` will hold the
1876 FakeGerritConnection object for a connection named `gerrit`.
1877
1878 :ivar FakeGearmanServer gearman_server: An instance of
1879 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1880 server that all of the Zuul components in this test use to
1881 communicate with each other.
1882
Paul Belanger174a8272017-03-14 13:20:10 -04001883 :ivar RecordingExecutorServer executor_server: An instance of
1884 :py:class:`~tests.base.RecordingExecutorServer` which is the
1885 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001886
1887 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1888 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001889 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001890 list upon completion.
1891
1892 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1893 objects representing completed builds. They are appended to
1894 the list in the order they complete.
1895
1896 """
1897
James E. Blair83005782015-12-11 14:46:03 -08001898 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001899 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001900 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001901 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001902
1903 def _startMerger(self):
1904 self.merge_server = zuul.merger.server.MergeServer(self.config,
1905 self.connections)
1906 self.merge_server.start()
1907
Maru Newby3fe5f852015-01-13 04:22:14 +00001908 def setUp(self):
1909 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001910
1911 self.setupZK()
1912
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001913 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001914 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001915 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1916 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001917 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001918 tmp_root = tempfile.mkdtemp(
1919 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001920 self.test_root = os.path.join(tmp_root, "zuul-test")
1921 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001922 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001923 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001924 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001925 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1926 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001927
1928 if os.path.exists(self.test_root):
1929 shutil.rmtree(self.test_root)
1930 os.makedirs(self.test_root)
1931 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001932 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001933 os.makedirs(self.merger_state_root)
1934 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001935
1936 # Make per test copy of Configuration.
1937 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001938 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1939 if not os.path.exists(self.private_key_file):
1940 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1941 shutil.copy(src_private_key_file, self.private_key_file)
1942 shutil.copy('{}.pub'.format(src_private_key_file),
1943 '{}.pub'.format(self.private_key_file))
1944 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001945 self.config.set('scheduler', 'tenant_config',
1946 os.path.join(
1947 FIXTURE_DIR,
1948 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001949 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001950 self.config.set(
1951 'scheduler', 'command_socket',
1952 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001953 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001954 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001955 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001956 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001957 self.config.set(
1958 'executor', 'command_socket',
1959 os.path.join(self.test_root, 'executor.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001960
Clark Boylanb640e052014-04-03 16:41:46 -07001961 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001962 if self.config.has_section('statsd'):
1963 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001964 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001965
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001966 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001967
1968 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001969 self.log.info("Gearman server on port %s" %
1970 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001971 if self.use_ssl:
1972 self.log.info('SSL enabled for gearman')
1973 self.config.set(
1974 'gearman', 'ssl_ca',
1975 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1976 self.config.set(
1977 'gearman', 'ssl_cert',
1978 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1979 self.config.set(
1980 'gearman', 'ssl_key',
1981 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001982
James E. Blaire511d2f2016-12-08 15:22:26 -08001983 gerritsource.GerritSource.replication_timeout = 1.5
1984 gerritsource.GerritSource.replication_retry_interval = 0.5
1985 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001986
Joshua Hesketh352264b2015-08-11 23:42:08 +10001987 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07001988 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07001989
Jan Hruban7083edd2015-08-21 14:00:54 +02001990 self.webapp = zuul.webapp.WebApp(
1991 self.sched, port=0, listen_address='127.0.0.1')
1992
Jan Hruban6b71aff2015-10-22 16:58:08 +02001993 self.event_queues = [
1994 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001995 self.sched.trigger_event_queue,
1996 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001997 ]
1998
James E. Blairfef78942016-03-11 16:28:56 -08001999 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002000 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002001
Paul Belanger174a8272017-03-14 13:20:10 -04002002 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002003 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002004 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002005 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002006 _test_root=self.test_root,
2007 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002008 self.executor_server.start()
2009 self.history = self.executor_server.build_history
2010 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002011
Paul Belanger174a8272017-03-14 13:20:10 -04002012 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002013 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002014 self.merge_client = zuul.merger.client.MergeClient(
2015 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002016 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002017 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002018 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002019
James E. Blair0d5a36e2017-02-21 10:53:44 -05002020 self.fake_nodepool = FakeNodepool(
2021 self.zk_chroot_fixture.zookeeper_host,
2022 self.zk_chroot_fixture.zookeeper_port,
2023 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002024
Paul Belanger174a8272017-03-14 13:20:10 -04002025 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002026 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002027 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002028 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002029
Clark Boylanb640e052014-04-03 16:41:46 -07002030 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002031 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002032 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002033 # Cleanups are run in reverse order
2034 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002035 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002036 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002037
James E. Blairb9c0d772017-03-03 14:34:49 -08002038 self.sched.reconfigure(self.config)
2039 self.sched.resume()
2040
Tobias Henkel7df274b2017-05-26 17:41:11 +02002041 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002042 # Set up gerrit related fakes
2043 # Set a changes database so multiple FakeGerrit's can report back to
2044 # a virtual canonical database given by the configured hostname
2045 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002046 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002047
2048 def getGerritConnection(driver, name, config):
2049 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2050 con = FakeGerritConnection(driver, name, config,
2051 changes_db=db,
2052 upstream_root=self.upstream_root)
2053 self.event_queues.append(con.event_queue)
2054 setattr(self, 'fake_' + name, con)
2055 return con
2056
2057 self.useFixture(fixtures.MonkeyPatch(
2058 'zuul.driver.gerrit.GerritDriver.getConnection',
2059 getGerritConnection))
2060
Gregory Haynes4fc12542015-04-22 20:38:06 -07002061 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002062 server = config.get('server', 'github.com')
2063 db = self.github_changes_dbs.setdefault(server, {})
Gregory Haynes4fc12542015-04-22 20:38:06 -07002064 con = FakeGithubConnection(driver, name, config,
James E. Blair6bacffb2018-01-05 13:45:25 -08002065 changes_db=db,
Gregory Haynes4fc12542015-04-22 20:38:06 -07002066 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002067 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002068 setattr(self, 'fake_' + name, con)
2069 return con
2070
2071 self.useFixture(fixtures.MonkeyPatch(
2072 'zuul.driver.github.GithubDriver.getConnection',
2073 getGithubConnection))
2074
James E. Blaire511d2f2016-12-08 15:22:26 -08002075 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002076 # TODO(jhesketh): This should come from lib.connections for better
2077 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002078 # Register connections from the config
2079 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002080
Joshua Hesketh352264b2015-08-11 23:42:08 +10002081 def FakeSMTPFactory(*args, **kw):
2082 args = [self.smtp_messages] + list(args)
2083 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002084
Joshua Hesketh352264b2015-08-11 23:42:08 +10002085 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002086
James E. Blaire511d2f2016-12-08 15:22:26 -08002087 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002088 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002089 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002090
James E. Blair83005782015-12-11 14:46:03 -08002091 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002092 # This creates the per-test configuration object. It can be
2093 # overriden by subclasses, but should not need to be since it
2094 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002095 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002096 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002097
James E. Blair39840362017-06-23 20:34:02 +01002098 sections = ['zuul', 'scheduler', 'executor', 'merger']
2099 for section in sections:
2100 if not self.config.has_section(section):
2101 self.config.add_section(section)
2102
James E. Blair06cc3922017-04-19 10:08:10 -07002103 if not self.setupSimpleLayout():
2104 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002105 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002106 self.tenant_config_file)
2107 git_path = os.path.join(
2108 os.path.dirname(
2109 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2110 'git')
2111 if os.path.exists(git_path):
2112 for reponame in os.listdir(git_path):
2113 project = reponame.replace('_', '/')
2114 self.copyDirToRepo(project,
2115 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002116 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002117 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002118 self.setupAllProjectKeys()
2119
James E. Blair06cc3922017-04-19 10:08:10 -07002120 def setupSimpleLayout(self):
2121 # If the test method has been decorated with a simple_layout,
2122 # use that instead of the class tenant_config_file. Set up a
2123 # single config-project with the specified layout, and
2124 # initialize repos for all of the 'project' entries which
2125 # appear in the layout.
2126 test_name = self.id().split('.')[-1]
2127 test = getattr(self, test_name)
2128 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002129 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002130 else:
2131 return False
2132
James E. Blairb70e55a2017-04-19 12:57:02 -07002133 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002134 path = os.path.join(FIXTURE_DIR, path)
2135 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002136 data = f.read()
2137 layout = yaml.safe_load(data)
2138 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002139 untrusted_projects = []
2140 for item in layout:
2141 if 'project' in item:
2142 name = item['project']['name']
2143 untrusted_projects.append(name)
2144 self.init_repo(name)
2145 self.addCommitToRepo(name, 'initial commit',
2146 files={'README': ''},
2147 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002148 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002149 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002150 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002151 for fn in zuul.configloader.as_list(
2152 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002153 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002154 for fn in zuul.configloader.as_list(
2155 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002156 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002157
2158 root = os.path.join(self.test_root, "config")
2159 if not os.path.exists(root):
2160 os.makedirs(root)
2161 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2162 config = [{'tenant':
2163 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002164 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002165 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002166 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002167 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002168 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002169 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002170 os.path.join(FIXTURE_DIR, f.name))
2171
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002172 self.init_repo('org/common-config')
2173 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002174 files, branch='master', tag='init')
2175
2176 return True
2177
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002178 def setupAllProjectKeys(self):
2179 if self.create_project_keys:
2180 return
2181
James E. Blair39840362017-06-23 20:34:02 +01002182 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002183 with open(os.path.join(FIXTURE_DIR, path)) as f:
2184 tenant_config = yaml.safe_load(f.read())
2185 for tenant in tenant_config:
2186 sources = tenant['tenant']['source']
2187 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002188 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002189 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002190 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002191 self.setupProjectKeys(source, project)
2192
2193 def setupProjectKeys(self, source, project):
2194 # Make sure we set up an RSA key for the project so that we
2195 # don't spend time generating one:
2196
James E. Blair6459db12017-06-29 14:57:20 -07002197 if isinstance(project, dict):
2198 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002199 key_root = os.path.join(self.state_root, 'keys')
2200 if not os.path.isdir(key_root):
2201 os.mkdir(key_root, 0o700)
2202 private_key_file = os.path.join(key_root, source, project + '.pem')
2203 private_key_dir = os.path.dirname(private_key_file)
2204 self.log.debug("Installing test keys for project %s at %s" % (
2205 project, private_key_file))
2206 if not os.path.isdir(private_key_dir):
2207 os.makedirs(private_key_dir)
2208 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2209 with open(private_key_file, 'w') as o:
2210 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002211
James E. Blair498059b2016-12-20 13:50:13 -08002212 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002213 self.zk_chroot_fixture = self.useFixture(
2214 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002215 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002216 self.zk_chroot_fixture.zookeeper_host,
2217 self.zk_chroot_fixture.zookeeper_port,
2218 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002219
James E. Blair96c6bf82016-01-15 16:20:40 -08002220 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002221 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002222
2223 files = {}
2224 for (dirpath, dirnames, filenames) in os.walk(source_path):
2225 for filename in filenames:
2226 test_tree_filepath = os.path.join(dirpath, filename)
2227 common_path = os.path.commonprefix([test_tree_filepath,
2228 source_path])
2229 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2230 with open(test_tree_filepath, 'r') as f:
2231 content = f.read()
2232 files[relative_filepath] = content
2233 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002234 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002235
James E. Blaire18d4602017-01-05 11:17:28 -08002236 def assertNodepoolState(self):
2237 # Make sure that there are no pending requests
2238
2239 requests = self.fake_nodepool.getNodeRequests()
2240 self.assertEqual(len(requests), 0)
2241
2242 nodes = self.fake_nodepool.getNodes()
2243 for node in nodes:
2244 self.assertFalse(node['_lock'], "Node %s is locked" %
2245 (node['_oid'],))
2246
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002247 def assertNoGeneratedKeys(self):
2248 # Make sure that Zuul did not generate any project keys
2249 # (unless it was supposed to).
2250
2251 if self.create_project_keys:
2252 return
2253
2254 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2255 test_key = i.read()
2256
2257 key_root = os.path.join(self.state_root, 'keys')
2258 for root, dirname, files in os.walk(key_root):
2259 for fn in files:
2260 with open(os.path.join(root, fn)) as f:
2261 self.assertEqual(test_key, f.read())
2262
Clark Boylanb640e052014-04-03 16:41:46 -07002263 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002264 self.log.debug("Assert final state")
2265 # Make sure no jobs are running
2266 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002267 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002268 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002269 gc.collect()
2270 for obj in gc.get_objects():
2271 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002272 self.log.debug("Leaked git repo object: 0x%x %s" %
2273 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002274 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002275 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002276 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002277 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002278 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002279 for tenant in self.sched.abide.tenants.values():
2280 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002281 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002282 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002283
2284 def shutdown(self):
2285 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002286 self.executor_server.hold_jobs_in_build = False
2287 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002288 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002289 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002290 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002291 self.sched.stop()
2292 self.sched.join()
2293 self.statsd.stop()
2294 self.statsd.join()
2295 self.webapp.stop()
2296 self.webapp.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002297 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002298 self.fake_nodepool.stop()
2299 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002300 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002301 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002302 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002303 # Further the pydevd threads also need to be whitelisted so debugging
2304 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002305 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002306 'pydevd.CommandThread',
2307 'pydevd.Reader',
2308 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002309 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002310 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002311 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002312 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002313 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002314 log_str = ""
2315 for thread_id, stack_frame in sys._current_frames().items():
2316 log_str += "Thread: %s\n" % thread_id
2317 log_str += "".join(traceback.format_stack(stack_frame))
2318 self.log.debug(log_str)
2319 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002320
James E. Blaira002b032017-04-18 10:35:48 -07002321 def assertCleanShutdown(self):
2322 pass
2323
James E. Blairc4ba97a2017-04-19 16:26:24 -07002324 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002325 parts = project.split('/')
2326 path = os.path.join(self.upstream_root, *parts[:-1])
2327 if not os.path.exists(path):
2328 os.makedirs(path)
2329 path = os.path.join(self.upstream_root, project)
2330 repo = git.Repo.init(path)
2331
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002332 with repo.config_writer() as config_writer:
2333 config_writer.set_value('user', 'email', 'user@example.com')
2334 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002335
Clark Boylanb640e052014-04-03 16:41:46 -07002336 repo.index.commit('initial commit')
2337 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002338 if tag:
2339 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002340
James E. Blair97d902e2014-08-21 13:25:56 -07002341 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002342 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002343 repo.git.clean('-x', '-f', '-d')
2344
James E. Blair97d902e2014-08-21 13:25:56 -07002345 def create_branch(self, project, branch):
2346 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002347 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002348 fn = os.path.join(path, 'README')
2349
2350 branch_head = repo.create_head(branch)
2351 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002352 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002353 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002354 f.close()
2355 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002356 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002357
James E. Blair97d902e2014-08-21 13:25:56 -07002358 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002359 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002360 repo.git.clean('-x', '-f', '-d')
2361
Sachi King9f16d522016-03-16 12:20:45 +11002362 def create_commit(self, project):
2363 path = os.path.join(self.upstream_root, project)
2364 repo = git.Repo(path)
2365 repo.head.reference = repo.heads['master']
2366 file_name = os.path.join(path, 'README')
2367 with open(file_name, 'a') as f:
2368 f.write('creating fake commit\n')
2369 repo.index.add([file_name])
2370 commit = repo.index.commit('Creating a fake commit')
2371 return commit.hexsha
2372
James E. Blairf4a5f022017-04-18 14:01:10 -07002373 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002374 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002375 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002376 while len(self.builds):
2377 self.release(self.builds[0])
2378 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002379 i += 1
2380 if count is not None and i >= count:
2381 break
James E. Blairb8c16472015-05-05 14:55:26 -07002382
James E. Blairdf25ddc2017-07-08 07:57:09 -07002383 def getSortedBuilds(self):
2384 "Return the list of currently running builds sorted by name"
2385
2386 return sorted(self.builds, key=lambda x: x.name)
2387
Clark Boylanb640e052014-04-03 16:41:46 -07002388 def release(self, job):
2389 if isinstance(job, FakeBuild):
2390 job.release()
2391 else:
2392 job.waiting = False
2393 self.log.debug("Queued job %s released" % job.unique)
2394 self.gearman_server.wakeConnections()
2395
2396 def getParameter(self, job, name):
2397 if isinstance(job, FakeBuild):
2398 return job.parameters[name]
2399 else:
2400 parameters = json.loads(job.arguments)
2401 return parameters[name]
2402
Clark Boylanb640e052014-04-03 16:41:46 -07002403 def haveAllBuildsReported(self):
2404 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002405 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002406 return False
2407 # Find out if every build that the worker has completed has been
2408 # reported back to Zuul. If it hasn't then that means a Gearman
2409 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002410 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002411 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002412 if not zbuild:
2413 # It has already been reported
2414 continue
2415 # It hasn't been reported yet.
2416 return False
2417 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002418 worker = self.executor_server.executor_worker
2419 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002420 if connection.state == 'GRAB_WAIT':
2421 return False
2422 return True
2423
2424 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002425 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002426 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002427 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002428 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002429 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002430 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002431 for j in conn.related_jobs.values():
2432 if j.unique == build.uuid:
2433 client_job = j
2434 break
2435 if not client_job:
2436 self.log.debug("%s is not known to the gearman client" %
2437 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002438 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002439 if not client_job.handle:
2440 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002441 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002442 server_job = self.gearman_server.jobs.get(client_job.handle)
2443 if not server_job:
2444 self.log.debug("%s is not known to the gearman server" %
2445 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002446 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002447 if not hasattr(server_job, 'waiting'):
2448 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002449 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002450 if server_job.waiting:
2451 continue
James E. Blair17302972016-08-10 16:11:42 -07002452 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002453 self.log.debug("%s has not reported start" % build)
2454 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002455 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002456 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002457 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002458 if worker_build:
2459 if worker_build.isWaiting():
2460 continue
2461 else:
2462 self.log.debug("%s is running" % worker_build)
2463 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002464 else:
James E. Blair962220f2016-08-03 11:22:38 -07002465 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002466 return False
James E. Blaira002b032017-04-18 10:35:48 -07002467 for (build_uuid, job_worker) in \
2468 self.executor_server.job_workers.items():
2469 if build_uuid not in seen_builds:
2470 self.log.debug("%s is not finalized" % build_uuid)
2471 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002472 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002473
James E. Blairdce6cea2016-12-20 16:45:32 -08002474 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002475 if self.fake_nodepool.paused:
2476 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002477 if self.sched.nodepool.requests:
2478 return False
2479 return True
2480
James E. Blaira615c362017-10-02 17:34:42 -07002481 def areAllMergeJobsWaiting(self):
2482 for client_job in list(self.merge_client.jobs):
2483 if not client_job.handle:
2484 self.log.debug("%s has no handle" % client_job)
2485 return False
2486 server_job = self.gearman_server.jobs.get(client_job.handle)
2487 if not server_job:
2488 self.log.debug("%s is not known to the gearman server" %
2489 client_job)
2490 return False
2491 if not hasattr(server_job, 'waiting'):
2492 self.log.debug("%s is being enqueued" % server_job)
2493 return False
2494 if server_job.waiting:
2495 self.log.debug("%s is waiting" % server_job)
2496 continue
2497 self.log.debug("%s is not waiting" % server_job)
2498 return False
2499 return True
2500
Jan Hruban6b71aff2015-10-22 16:58:08 +02002501 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002502 for event_queue in self.event_queues:
2503 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002504
2505 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002506 for event_queue in self.event_queues:
2507 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002508
Clark Boylanb640e052014-04-03 16:41:46 -07002509 def waitUntilSettled(self):
2510 self.log.debug("Waiting until settled...")
2511 start = time.time()
2512 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002513 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002514 self.log.error("Timeout waiting for Zuul to settle")
2515 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002516 for event_queue in self.event_queues:
2517 self.log.error(" %s: %s" %
2518 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002519 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002520 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002521 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002522 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002523 self.log.error("All requests completed: %s" %
2524 (self.areAllNodeRequestsComplete(),))
2525 self.log.error("Merge client jobs: %s" %
2526 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002527 raise Exception("Timeout waiting for Zuul to settle")
2528 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002529
Paul Belanger174a8272017-03-14 13:20:10 -04002530 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002531 # have all build states propogated to zuul?
2532 if self.haveAllBuildsReported():
2533 # Join ensures that the queue is empty _and_ events have been
2534 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002535 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002536 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002537 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002538 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002539 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002540 self.areAllNodeRequestsComplete() and
2541 all(self.eventQueuesEmpty())):
2542 # The queue empty check is placed at the end to
2543 # ensure that if a component adds an event between
2544 # when locked the run handler and checked that the
2545 # components were stable, we don't erroneously
2546 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002547 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002548 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002549 self.log.debug("...settled.")
2550 return
2551 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002552 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002553 self.sched.wake_event.wait(0.1)
2554
2555 def countJobResults(self, jobs, result):
2556 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002557 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002558
Monty Taylor0d926122017-05-24 08:07:56 -05002559 def getBuildByName(self, name):
2560 for build in self.builds:
2561 if build.name == name:
2562 return build
2563 raise Exception("Unable to find build %s" % name)
2564
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002565 def assertJobNotInHistory(self, name, project=None):
2566 for job in self.history:
2567 if (project is None or
2568 job.parameters['zuul']['project']['name'] == project):
2569 self.assertNotEqual(job.name, name,
2570 'Job %s found in history' % name)
2571
James E. Blair96c6bf82016-01-15 16:20:40 -08002572 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002573 for job in self.history:
2574 if (job.name == name and
2575 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002576 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002577 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002578 raise Exception("Unable to find job %s in history" % name)
2579
2580 def assertEmptyQueues(self):
2581 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002582 for tenant in self.sched.abide.tenants.values():
2583 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002584 for pipeline_queue in pipeline.queues:
2585 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002586 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002587 pipeline.name, pipeline_queue.name,
2588 pipeline_queue.queue))
2589 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002590 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002591
2592 def assertReportedStat(self, key, value=None, kind=None):
2593 start = time.time()
2594 while time.time() < (start + 5):
2595 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002596 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002597 if key == k:
2598 if value is None and kind is None:
2599 return
2600 elif value:
2601 if value == v:
2602 return
2603 elif kind:
2604 if v.endswith('|' + kind):
2605 return
2606 time.sleep(0.1)
2607
Clark Boylanb640e052014-04-03 16:41:46 -07002608 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002609
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002610 def assertBuilds(self, builds):
2611 """Assert that the running builds are as described.
2612
2613 The list of running builds is examined and must match exactly
2614 the list of builds described by the input.
2615
2616 :arg list builds: A list of dictionaries. Each item in the
2617 list must match the corresponding build in the build
2618 history, and each element of the dictionary must match the
2619 corresponding attribute of the build.
2620
2621 """
James E. Blair3158e282016-08-19 09:34:11 -07002622 try:
2623 self.assertEqual(len(self.builds), len(builds))
2624 for i, d in enumerate(builds):
2625 for k, v in d.items():
2626 self.assertEqual(
2627 getattr(self.builds[i], k), v,
2628 "Element %i in builds does not match" % (i,))
2629 except Exception:
2630 for build in self.builds:
2631 self.log.error("Running build: %s" % build)
2632 else:
2633 self.log.error("No running builds")
2634 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002635
James E. Blairb536ecc2016-08-31 10:11:42 -07002636 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002637 """Assert that the completed builds are as described.
2638
2639 The list of completed builds is examined and must match
2640 exactly the list of builds described by the input.
2641
2642 :arg list history: A list of dictionaries. Each item in the
2643 list must match the corresponding build in the build
2644 history, and each element of the dictionary must match the
2645 corresponding attribute of the build.
2646
James E. Blairb536ecc2016-08-31 10:11:42 -07002647 :arg bool ordered: If true, the history must match the order
2648 supplied, if false, the builds are permitted to have
2649 arrived in any order.
2650
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002651 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002652 def matches(history_item, item):
2653 for k, v in item.items():
2654 if getattr(history_item, k) != v:
2655 return False
2656 return True
James E. Blair3158e282016-08-19 09:34:11 -07002657 try:
2658 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002659 if ordered:
2660 for i, d in enumerate(history):
2661 if not matches(self.history[i], d):
2662 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002663 "Element %i in history does not match %s" %
2664 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002665 else:
2666 unseen = self.history[:]
2667 for i, d in enumerate(history):
2668 found = False
2669 for unseen_item in unseen:
2670 if matches(unseen_item, d):
2671 found = True
2672 unseen.remove(unseen_item)
2673 break
2674 if not found:
2675 raise Exception("No match found for element %i "
2676 "in history" % (i,))
2677 if unseen:
2678 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002679 except Exception:
2680 for build in self.history:
2681 self.log.error("Completed build: %s" % build)
2682 else:
2683 self.log.error("No completed builds")
2684 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002685
James E. Blair6ac368c2016-12-22 18:07:20 -08002686 def printHistory(self):
2687 """Log the build history.
2688
2689 This can be useful during tests to summarize what jobs have
2690 completed.
2691
2692 """
2693 self.log.debug("Build history:")
2694 for build in self.history:
2695 self.log.debug(build)
2696
James E. Blair59fdbac2015-12-07 17:08:06 -08002697 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002698 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2699
James E. Blair9ea70072017-04-19 16:05:30 -07002700 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002701 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002702 if not os.path.exists(root):
2703 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002704 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2705 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002706- tenant:
2707 name: openstack
2708 source:
2709 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002710 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002711 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002712 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002713 - org/project
2714 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002715 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002716 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002717 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002718 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002719 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002720
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002721 def addTagToRepo(self, project, name, sha):
2722 path = os.path.join(self.upstream_root, project)
2723 repo = git.Repo(path)
2724 repo.git.tag(name, sha)
2725
2726 def delTagFromRepo(self, project, name):
2727 path = os.path.join(self.upstream_root, project)
2728 repo = git.Repo(path)
2729 repo.git.tag('-d', name)
2730
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002731 def addCommitToRepo(self, project, message, files,
2732 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002733 path = os.path.join(self.upstream_root, project)
2734 repo = git.Repo(path)
2735 repo.head.reference = branch
2736 zuul.merger.merger.reset_repo_to_head(repo)
2737 for fn, content in files.items():
2738 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002739 try:
2740 os.makedirs(os.path.dirname(fn))
2741 except OSError:
2742 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002743 with open(fn, 'w') as f:
2744 f.write(content)
2745 repo.index.add([fn])
2746 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002747 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002748 repo.heads[branch].commit = commit
2749 repo.head.reference = branch
2750 repo.git.clean('-x', '-f', '-d')
2751 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002752 if tag:
2753 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002754 return before
2755
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002756 def commitConfigUpdate(self, project_name, source_name):
2757 """Commit an update to zuul.yaml
2758
2759 This overwrites the zuul.yaml in the specificed project with
2760 the contents specified.
2761
2762 :arg str project_name: The name of the project containing
2763 zuul.yaml (e.g., common-config)
2764
2765 :arg str source_name: The path to the file (underneath the
2766 test fixture directory) whose contents should be used to
2767 replace zuul.yaml.
2768 """
2769
2770 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002771 files = {}
2772 with open(source_path, 'r') as f:
2773 data = f.read()
2774 layout = yaml.safe_load(data)
2775 files['zuul.yaml'] = data
2776 for item in layout:
2777 if 'job' in item:
2778 jobname = item['job']['name']
2779 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002780 before = self.addCommitToRepo(
2781 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002782 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002783 return before
2784
Clint Byrum627ba362017-08-14 13:20:40 -07002785 def newTenantConfig(self, source_name):
2786 """ Use this to update the tenant config file in tests
2787
2788 This will update self.tenant_config_file to point to a temporary file
2789 for the duration of this particular test. The content of that file will
2790 be taken from FIXTURE_DIR/source_name
2791
2792 After the test the original value of self.tenant_config_file will be
2793 restored.
2794
2795 :arg str source_name: The path of the file under
2796 FIXTURE_DIR that will be used to populate the new tenant
2797 config file.
2798 """
2799 source_path = os.path.join(FIXTURE_DIR, source_name)
2800 orig_tenant_config_file = self.tenant_config_file
2801 with tempfile.NamedTemporaryFile(
2802 delete=False, mode='wb') as new_tenant_config:
2803 self.tenant_config_file = new_tenant_config.name
2804 with open(source_path, mode='rb') as source_tenant_config:
2805 new_tenant_config.write(source_tenant_config.read())
2806 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2807 self.setupAllProjectKeys()
2808 self.log.debug(
2809 'tenant_config_file = {}'.format(self.tenant_config_file))
2810
2811 def _restoreTenantConfig():
2812 self.log.debug(
2813 'restoring tenant_config_file = {}'.format(
2814 orig_tenant_config_file))
2815 os.unlink(self.tenant_config_file)
2816 self.tenant_config_file = orig_tenant_config_file
2817 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2818 self.addCleanup(_restoreTenantConfig)
2819
James E. Blair7fc8daa2016-08-08 15:37:15 -07002820 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002821
James E. Blair7fc8daa2016-08-08 15:37:15 -07002822 """Inject a Fake (Gerrit) event.
2823
2824 This method accepts a JSON-encoded event and simulates Zuul
2825 having received it from Gerrit. It could (and should)
2826 eventually apply to any connection type, but is currently only
2827 used with Gerrit connections. The name of the connection is
2828 used to look up the corresponding server, and the event is
2829 simulated as having been received by all Zuul connections
2830 attached to that server. So if two Gerrit connections in Zuul
2831 are connected to the same Gerrit server, and you invoke this
2832 method specifying the name of one of them, the event will be
2833 received by both.
2834
2835 .. note::
2836
2837 "self.fake_gerrit.addEvent" calls should be migrated to
2838 this method.
2839
2840 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002841 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002842 :arg str event: The JSON-encoded event.
2843
2844 """
2845 specified_conn = self.connections.connections[connection]
2846 for conn in self.connections.connections.values():
2847 if (isinstance(conn, specified_conn.__class__) and
2848 specified_conn.server == conn.server):
2849 conn.addEvent(event)
2850
James E. Blaird8af5422017-05-24 13:59:40 -07002851 def getUpstreamRepos(self, projects):
2852 """Return upstream git repo objects for the listed projects
2853
2854 :arg list projects: A list of strings, each the canonical name
2855 of a project.
2856
2857 :returns: A dictionary of {name: repo} for every listed
2858 project.
2859 :rtype: dict
2860
2861 """
2862
2863 repos = {}
2864 for project in projects:
2865 # FIXME(jeblair): the upstream root does not yet have a
2866 # hostname component; that needs to be added, and this
2867 # line removed:
2868 tmp_project_name = '/'.join(project.split('/')[1:])
2869 path = os.path.join(self.upstream_root, tmp_project_name)
2870 repo = git.Repo(path)
2871 repos[project] = repo
2872 return repos
2873
James E. Blair3f876d52016-07-22 13:07:14 -07002874
2875class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002876 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002877 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002878
Jamie Lennox7655b552017-03-17 12:33:38 +11002879 @contextmanager
2880 def jobLog(self, build):
2881 """Print job logs on assertion errors
2882
2883 This method is a context manager which, if it encounters an
2884 ecxeption, adds the build log to the debug output.
2885
2886 :arg Build build: The build that's being asserted.
2887 """
2888 try:
2889 yield
2890 except Exception:
2891 path = os.path.join(self.test_root, build.uuid,
2892 'work', 'logs', 'job-output.txt')
2893 with open(path) as f:
2894 self.log.debug(f.read())
2895 raise
2896
Joshua Heskethd78b4482015-09-14 16:56:34 -06002897
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002898class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002899 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002900 use_ssl = True
2901
2902
Joshua Heskethd78b4482015-09-14 16:56:34 -06002903class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002904 def setup_config(self):
2905 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002906 for section_name in self.config.sections():
2907 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2908 section_name, re.I)
2909 if not con_match:
2910 continue
2911
2912 if self.config.get(section_name, 'driver') == 'sql':
2913 f = MySQLSchemaFixture()
2914 self.useFixture(f)
2915 if (self.config.get(section_name, 'dburi') ==
2916 '$MYSQL_FIXTURE_DBURI$'):
2917 self.config.set(section_name, 'dburi', f.dburi)