blob: 3dba081835feec2fe7ea1ffbad8b61ebb1c6377f [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 -050023import importlib
24from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070025import json
26import logging
27import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050028import queue
Clark Boylanb640e052014-04-03 16:41:46 -070029import random
30import re
31import select
32import shutil
33import socket
34import string
35import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
Clark Boylan8208c192017-04-24 18:08:08 -070039import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070040import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060041import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050042import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060043
Clark Boylanb640e052014-04-03 16:41:46 -070044
45import git
46import gear
47import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080048import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080049import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060050import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070051import statsd
52import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080053import testtools.content
54import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080055from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000056import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070057
James E. Blaire511d2f2016-12-08 15:22:26 -080058import zuul.driver.gerrit.gerritsource as gerritsource
59import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070060import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.scheduler
62import zuul.webapp
63import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040064import zuul.executor.server
65import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080066import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070067import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070068import zuul.merger.merger
69import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020070import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200137 categories = {'Approved': ('Approved', -1, 1),
138 'Code-Review': ('Code-Review', -2, 2),
139 'Verified': ('Verified', -2, 2)}
140
141 # TODO(tobiash): This is used as a translation layer between the tests
142 # which use lower case labels. This can be removed if all
143 # tests are converted to use the correct casing.
144 categories_translation = {'approved': 'Approved',
145 'code-review': 'Code-Review',
146 'verified': 'Verified',
147 'Approved': 'Approved',
148 'Code-Review': 'Code-Review',
149 'Verified': 'Verified'}
Clark Boylanb640e052014-04-03 16:41:46 -0700150
151 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700152 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700153 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700154 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700155 self.reported = 0
156 self.queried = 0
157 self.patchsets = []
158 self.number = number
159 self.project = project
160 self.branch = branch
161 self.subject = subject
162 self.latest_patchset = 0
163 self.depends_on_change = None
164 self.needed_by_changes = []
165 self.fail_merge = False
166 self.messages = []
167 self.data = {
168 'branch': branch,
169 'comments': [],
170 'commitMessage': subject,
171 'createdOn': time.time(),
172 'id': 'I' + random_sha1(),
173 'lastUpdated': time.time(),
174 'number': str(number),
175 'open': status == 'NEW',
176 'owner': {'email': 'user@example.com',
177 'name': 'User Name',
178 'username': 'username'},
179 'patchSets': self.patchsets,
180 'project': project,
181 'status': status,
182 'subject': subject,
183 'submitRecords': [],
184 'url': 'https://hostname/%s' % number}
185
186 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700187 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 self.data['submitRecords'] = self.getSubmitRecords()
189 self.open = status == 'NEW'
190
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700191 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700192 path = os.path.join(self.upstream_root, self.project)
193 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700194 ref = GerritChangeReference.create(
195 repo, '1/%s/%s' % (self.number, self.latest_patchset),
196 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700197 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700198 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700199 repo.git.clean('-x', '-f', '-d')
200
201 path = os.path.join(self.upstream_root, self.project)
202 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700203 for fn, content in files.items():
204 fn = os.path.join(path, fn)
205 with open(fn, 'w') as f:
206 f.write(content)
207 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700208 else:
209 for fni in range(100):
210 fn = os.path.join(path, str(fni))
211 f = open(fn, 'w')
212 for ci in range(4096):
213 f.write(random.choice(string.printable))
214 f.close()
215 repo.index.add([fn])
216
217 r = repo.index.commit(msg)
218 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700219 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700220 repo.git.clean('-x', '-f', '-d')
221 repo.heads['master'].checkout()
222 return r
223
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700225 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700227 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700228 data = ("test %s %s %s\n" %
229 (self.branch, self.number, self.latest_patchset))
230 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700231 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700232 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700233 ps_files = [{'file': '/COMMIT_MSG',
234 'type': 'ADDED'},
235 {'file': 'README',
236 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700237 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700238 ps_files.append({'file': f, 'type': 'ADDED'})
239 d = {'approvals': [],
240 'createdOn': time.time(),
241 'files': ps_files,
242 'number': str(self.latest_patchset),
243 'ref': 'refs/changes/1/%s/%s' % (self.number,
244 self.latest_patchset),
245 'revision': c.hexsha,
246 'uploader': {'email': 'user@example.com',
247 'name': 'User name',
248 'username': 'user'}}
249 self.data['currentPatchSet'] = d
250 self.patchsets.append(d)
251 self.data['submitRecords'] = self.getSubmitRecords()
252
253 def getPatchsetCreatedEvent(self, patchset):
254 event = {"type": "patchset-created",
255 "change": {"project": self.project,
256 "branch": self.branch,
257 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
258 "number": str(self.number),
259 "subject": self.subject,
260 "owner": {"name": "User Name"},
261 "url": "https://hostname/3"},
262 "patchSet": self.patchsets[patchset - 1],
263 "uploader": {"name": "User Name"}}
264 return event
265
266 def getChangeRestoredEvent(self):
267 event = {"type": "change-restored",
268 "change": {"project": self.project,
269 "branch": self.branch,
270 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
271 "number": str(self.number),
272 "subject": self.subject,
273 "owner": {"name": "User Name"},
274 "url": "https://hostname/3"},
275 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100276 "patchSet": self.patchsets[-1],
277 "reason": ""}
278 return event
279
280 def getChangeAbandonedEvent(self):
281 event = {"type": "change-abandoned",
282 "change": {"project": self.project,
283 "branch": self.branch,
284 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
285 "number": str(self.number),
286 "subject": self.subject,
287 "owner": {"name": "User Name"},
288 "url": "https://hostname/3"},
289 "abandoner": {"name": "User Name"},
290 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700291 "reason": ""}
292 return event
293
294 def getChangeCommentEvent(self, patchset):
295 event = {"type": "comment-added",
296 "change": {"project": self.project,
297 "branch": self.branch,
298 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
299 "number": str(self.number),
300 "subject": self.subject,
301 "owner": {"name": "User Name"},
302 "url": "https://hostname/3"},
303 "patchSet": self.patchsets[patchset - 1],
304 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700305 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700306 "description": "Code-Review",
307 "value": "0"}],
308 "comment": "This is a comment"}
309 return event
310
James E. Blairc2a5ed72017-02-20 14:12:01 -0500311 def getChangeMergedEvent(self):
312 event = {"submitter": {"name": "Jenkins",
313 "username": "jenkins"},
314 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
315 "patchSet": self.patchsets[-1],
316 "change": self.data,
317 "type": "change-merged",
318 "eventCreatedOn": 1487613810}
319 return event
320
James E. Blair8cce42e2016-10-18 08:18:36 -0700321 def getRefUpdatedEvent(self):
322 path = os.path.join(self.upstream_root, self.project)
323 repo = git.Repo(path)
324 oldrev = repo.heads[self.branch].commit.hexsha
325
326 event = {
327 "type": "ref-updated",
328 "submitter": {
329 "name": "User Name",
330 },
331 "refUpdate": {
332 "oldRev": oldrev,
333 "newRev": self.patchsets[-1]['revision'],
334 "refName": self.branch,
335 "project": self.project,
336 }
337 }
338 return event
339
Joshua Hesketh642824b2014-07-01 17:54:59 +1000340 def addApproval(self, category, value, username='reviewer_john',
341 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700342 if not granted_on:
343 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000344 approval = {
Tobias Henkelea98a192017-05-29 21:15:17 +0200345 'description': self.categories_translation[category],
346 'type': self.categories_translation[category],
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000347 'value': str(value),
348 'by': {
349 'username': username,
350 'email': username + '@example.com',
351 },
352 'grantedOn': int(granted_on)
353 }
Clark Boylanb640e052014-04-03 16:41:46 -0700354 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelea98a192017-05-29 21:15:17 +0200355 if x['by']['username'] == username and \
356 x['type'] == self.categories_translation[category]:
Clark Boylanb640e052014-04-03 16:41:46 -0700357 del self.patchsets[-1]['approvals'][i]
358 self.patchsets[-1]['approvals'].append(approval)
359 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000360 'author': {'email': 'author@example.com',
361 'name': 'Patchset Author',
362 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'change': {'branch': self.branch,
364 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
365 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000366 'owner': {'email': 'owner@example.com',
367 'name': 'Change Owner',
368 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700369 'project': self.project,
370 'subject': self.subject,
371 'topic': 'master',
372 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000373 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700374 'patchSet': self.patchsets[-1],
375 'type': 'comment-added'}
376 self.data['submitRecords'] = self.getSubmitRecords()
377 return json.loads(json.dumps(event))
378
379 def getSubmitRecords(self):
380 status = {}
381 for cat in self.categories.keys():
382 status[cat] = 0
383
384 for a in self.patchsets[-1]['approvals']:
385 cur = status[a['type']]
386 cat_min, cat_max = self.categories[a['type']][1:]
387 new = int(a['value'])
388 if new == cat_min:
389 cur = new
390 elif abs(new) > abs(cur):
391 cur = new
392 status[a['type']] = cur
393
394 labels = []
395 ok = True
396 for typ, cat in self.categories.items():
397 cur = status[typ]
398 cat_min, cat_max = cat[1:]
399 if cur == cat_min:
400 value = 'REJECT'
401 ok = False
402 elif cur == cat_max:
403 value = 'OK'
404 else:
405 value = 'NEED'
406 ok = False
407 labels.append({'label': cat[0], 'status': value})
408 if ok:
409 return [{'status': 'OK'}]
410 return [{'status': 'NOT_READY',
411 'labels': labels}]
412
413 def setDependsOn(self, other, patchset):
414 self.depends_on_change = other
415 d = {'id': other.data['id'],
416 'number': other.data['number'],
417 'ref': other.patchsets[patchset - 1]['ref']
418 }
419 self.data['dependsOn'] = [d]
420
421 other.needed_by_changes.append(self)
422 needed = other.data.get('neededBy', [])
423 d = {'id': self.data['id'],
424 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700425 'ref': self.patchsets[-1]['ref'],
426 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700427 }
428 needed.append(d)
429 other.data['neededBy'] = needed
430
431 def query(self):
432 self.queried += 1
433 d = self.data.get('dependsOn')
434 if d:
435 d = d[0]
436 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
437 d['isCurrentPatchSet'] = True
438 else:
439 d['isCurrentPatchSet'] = False
440 return json.loads(json.dumps(self.data))
441
442 def setMerged(self):
443 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000444 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700445 return
446 if self.fail_merge:
447 return
448 self.data['status'] = 'MERGED'
449 self.open = False
450
451 path = os.path.join(self.upstream_root, self.project)
452 repo = git.Repo(path)
453 repo.heads[self.branch].commit = \
454 repo.commit(self.patchsets[-1]['revision'])
455
456 def setReported(self):
457 self.reported += 1
458
459
James E. Blaire511d2f2016-12-08 15:22:26 -0800460class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700461 """A Fake Gerrit connection for use in tests.
462
463 This subclasses
464 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
465 ability for tests to add changes to the fake Gerrit it represents.
466 """
467
Joshua Hesketh352264b2015-08-11 23:42:08 +1000468 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700469
James E. Blaire511d2f2016-12-08 15:22:26 -0800470 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700471 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800472 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000473 connection_config)
474
Monty Taylorb934c1a2017-06-16 19:31:47 -0500475 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700476 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
477 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000478 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700479 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200480 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700481
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700482 def addFakeChange(self, project, branch, subject, status='NEW',
483 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700484 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700485 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700486 c = FakeGerritChange(self, self.change_number, project, branch,
487 subject, upstream_root=self.upstream_root,
488 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700489 self.changes[self.change_number] = c
490 return c
491
Clark Boylanb640e052014-04-03 16:41:46 -0700492 def review(self, project, changeid, message, action):
493 number, ps = changeid.split(',')
494 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000495
496 # Add the approval back onto the change (ie simulate what gerrit would
497 # do).
498 # Usually when zuul leaves a review it'll create a feedback loop where
499 # zuul's review enters another gerrit event (which is then picked up by
500 # zuul). However, we can't mimic this behaviour (by adding this
501 # approval event into the queue) as it stops jobs from checking what
502 # happens before this event is triggered. If a job needs to see what
503 # happens they can add their own verified event into the queue.
504 # Nevertheless, we can update change with the new review in gerrit.
505
James E. Blair8b5408c2016-08-08 15:37:46 -0700506 for cat in action.keys():
507 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000508 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000509
Clark Boylanb640e052014-04-03 16:41:46 -0700510 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000511
Clark Boylanb640e052014-04-03 16:41:46 -0700512 if 'submit' in action:
513 change.setMerged()
514 if message:
515 change.setReported()
516
517 def query(self, number):
518 change = self.changes.get(int(number))
519 if change:
520 return change.query()
521 return {}
522
James E. Blairc494d542014-08-06 09:23:52 -0700523 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700524 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700525 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800526 if query.startswith('change:'):
527 # Query a specific changeid
528 changeid = query[len('change:'):]
529 l = [change.query() for change in self.changes.values()
530 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700531 elif query.startswith('message:'):
532 # Query the content of a commit message
533 msg = query[len('message:'):].strip()
534 l = [change.query() for change in self.changes.values()
535 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800536 else:
537 # Query all open changes
538 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700539 return l
James E. Blairc494d542014-08-06 09:23:52 -0700540
Joshua Hesketh352264b2015-08-11 23:42:08 +1000541 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700542 pass
543
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200544 def _uploadPack(self, project):
545 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
546 'multi_ack thin-pack side-band side-band-64k ofs-delta '
547 'shallow no-progress include-tag multi_ack_detailed no-done\n')
548 path = os.path.join(self.upstream_root, project.name)
549 repo = git.Repo(path)
550 for ref in repo.refs:
551 r = ref.object.hexsha + ' ' + ref.path + '\n'
552 ret += '%04x%s' % (len(r) + 4, r)
553 ret += '0000'
554 return ret
555
Joshua Hesketh352264b2015-08-11 23:42:08 +1000556 def getGitUrl(self, project):
557 return os.path.join(self.upstream_root, project.name)
558
Clark Boylanb640e052014-04-03 16:41:46 -0700559
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560class GithubChangeReference(git.Reference):
561 _common_path_default = "refs/pull"
562 _points_to_commits_only = True
563
564
565class FakeGithubPullRequest(object):
566
567 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800568 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700569 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700570 """Creates a new PR with several commits.
571 Sends an event about opened PR."""
572 self.github = github
573 self.source = github
574 self.number = number
575 self.project = project
576 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100577 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700578 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100579 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700580 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100581 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100583 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100584 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800585 self.reviews = []
586 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700587 self.updated_at = None
588 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100589 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100590 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700591 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700592 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100593 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700594 self._updateTimeStamp()
595
Jan Hruban570d01c2016-03-10 21:51:32 +0100596 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700597 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100598 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700599 self._updateTimeStamp()
600
Jan Hruban570d01c2016-03-10 21:51:32 +0100601 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700602 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100603 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700604 self._updateTimeStamp()
605
606 def getPullRequestOpenedEvent(self):
607 return self._getPullRequestEvent('opened')
608
609 def getPullRequestSynchronizeEvent(self):
610 return self._getPullRequestEvent('synchronize')
611
612 def getPullRequestReopenedEvent(self):
613 return self._getPullRequestEvent('reopened')
614
615 def getPullRequestClosedEvent(self):
616 return self._getPullRequestEvent('closed')
617
Jesse Keatinga41566f2017-06-14 18:17:51 -0700618 def getPullRequestEditedEvent(self):
619 return self._getPullRequestEvent('edited')
620
Gregory Haynes4fc12542015-04-22 20:38:06 -0700621 def addComment(self, message):
622 self.comments.append(message)
623 self._updateTimeStamp()
624
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200625 def getCommentAddedEvent(self, text):
626 name = 'issue_comment'
627 data = {
628 'action': 'created',
629 'issue': {
630 'number': self.number
631 },
632 'comment': {
633 'body': text
634 },
635 'repository': {
636 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100637 },
638 'sender': {
639 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200640 }
641 }
642 return (name, data)
643
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800644 def getReviewAddedEvent(self, review):
645 name = 'pull_request_review'
646 data = {
647 'action': 'submitted',
648 'pull_request': {
649 'number': self.number,
650 'title': self.subject,
651 'updated_at': self.updated_at,
652 'base': {
653 'ref': self.branch,
654 'repo': {
655 'full_name': self.project
656 }
657 },
658 'head': {
659 'sha': self.head_sha
660 }
661 },
662 'review': {
663 'state': review
664 },
665 'repository': {
666 'full_name': self.project
667 },
668 'sender': {
669 'login': 'ghuser'
670 }
671 }
672 return (name, data)
673
Jan Hruban16ad31f2015-11-07 14:39:07 +0100674 def addLabel(self, name):
675 if name not in self.labels:
676 self.labels.append(name)
677 self._updateTimeStamp()
678 return self._getLabelEvent(name)
679
680 def removeLabel(self, name):
681 if name in self.labels:
682 self.labels.remove(name)
683 self._updateTimeStamp()
684 return self._getUnlabelEvent(name)
685
686 def _getLabelEvent(self, label):
687 name = 'pull_request'
688 data = {
689 'action': 'labeled',
690 'pull_request': {
691 'number': self.number,
692 'updated_at': self.updated_at,
693 'base': {
694 'ref': self.branch,
695 'repo': {
696 'full_name': self.project
697 }
698 },
699 'head': {
700 'sha': self.head_sha
701 }
702 },
703 'label': {
704 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100705 },
706 'sender': {
707 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100708 }
709 }
710 return (name, data)
711
712 def _getUnlabelEvent(self, label):
713 name = 'pull_request'
714 data = {
715 'action': 'unlabeled',
716 'pull_request': {
717 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100718 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100719 'updated_at': self.updated_at,
720 'base': {
721 'ref': self.branch,
722 'repo': {
723 'full_name': self.project
724 }
725 },
726 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800727 'sha': self.head_sha,
728 'repo': {
729 'full_name': self.project
730 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100731 }
732 },
733 'label': {
734 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100735 },
736 'sender': {
737 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100738 }
739 }
740 return (name, data)
741
Jesse Keatinga41566f2017-06-14 18:17:51 -0700742 def editBody(self, body):
743 self.body = body
744 self._updateTimeStamp()
745
Gregory Haynes4fc12542015-04-22 20:38:06 -0700746 def _getRepo(self):
747 repo_path = os.path.join(self.upstream_root, self.project)
748 return git.Repo(repo_path)
749
750 def _createPRRef(self):
751 repo = self._getRepo()
752 GithubChangeReference.create(
753 repo, self._getPRReference(), 'refs/tags/init')
754
Jan Hruban570d01c2016-03-10 21:51:32 +0100755 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700756 repo = self._getRepo()
757 ref = repo.references[self._getPRReference()]
758 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100759 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700760 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100761 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700762 repo.head.reference = ref
763 zuul.merger.merger.reset_repo_to_head(repo)
764 repo.git.clean('-x', '-f', '-d')
765
Jan Hruban570d01c2016-03-10 21:51:32 +0100766 if files:
767 fn = files[0]
768 self.files = files
769 else:
770 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
771 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100772 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700773 fn = os.path.join(repo.working_dir, fn)
774 f = open(fn, 'w')
775 with open(fn, 'w') as f:
776 f.write("test %s %s\n" %
777 (self.branch, self.number))
778 repo.index.add([fn])
779
780 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800781 # Create an empty set of statuses for the given sha,
782 # each sha on a PR may have a status set on it
783 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700784 repo.head.reference = 'master'
785 zuul.merger.merger.reset_repo_to_head(repo)
786 repo.git.clean('-x', '-f', '-d')
787 repo.heads['master'].checkout()
788
789 def _updateTimeStamp(self):
790 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
791
792 def getPRHeadSha(self):
793 repo = self._getRepo()
794 return repo.references[self._getPRReference()].commit.hexsha
795
Jesse Keatingae4cd272017-01-30 17:10:44 -0800796 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800797 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
798 # convert the timestamp to a str format that would be returned
799 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800800
Adam Gandelmand81dd762017-02-09 15:15:49 -0800801 if granted_on:
802 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
803 submitted_at = time.strftime(
804 gh_time_format, granted_on.timetuple())
805 else:
806 # github timestamps only down to the second, so we need to make
807 # sure reviews that tests add appear to be added over a period of
808 # time in the past and not all at once.
809 if not self.reviews:
810 # the first review happens 10 mins ago
811 offset = 600
812 else:
813 # subsequent reviews happen 1 minute closer to now
814 offset = 600 - (len(self.reviews) * 60)
815
816 granted_on = datetime.datetime.utcfromtimestamp(
817 time.time() - offset)
818 submitted_at = time.strftime(
819 gh_time_format, granted_on.timetuple())
820
Jesse Keatingae4cd272017-01-30 17:10:44 -0800821 self.reviews.append({
822 'state': state,
823 'user': {
824 'login': user,
825 'email': user + "@derp.com",
826 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800827 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800828 })
829
Gregory Haynes4fc12542015-04-22 20:38:06 -0700830 def _getPRReference(self):
831 return '%s/head' % self.number
832
833 def _getPullRequestEvent(self, action):
834 name = 'pull_request'
835 data = {
836 'action': action,
837 'number': self.number,
838 'pull_request': {
839 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100840 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700841 'updated_at': self.updated_at,
842 'base': {
843 'ref': self.branch,
844 'repo': {
845 'full_name': self.project
846 }
847 },
848 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800849 'sha': self.head_sha,
850 'repo': {
851 'full_name': self.project
852 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700853 },
854 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100855 },
856 'sender': {
857 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700858 }
859 }
860 return (name, data)
861
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800862 def getCommitStatusEvent(self, context, state='success', user='zuul'):
863 name = 'status'
864 data = {
865 'state': state,
866 'sha': self.head_sha,
867 'description': 'Test results for %s: %s' % (self.head_sha, state),
868 'target_url': 'http://zuul/%s' % self.head_sha,
869 'branches': [],
870 'context': context,
871 'sender': {
872 'login': user
873 }
874 }
875 return (name, data)
876
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877
878class FakeGithubConnection(githubconnection.GithubConnection):
879 log = logging.getLogger("zuul.test.FakeGithubConnection")
880
881 def __init__(self, driver, connection_name, connection_config,
882 upstream_root=None):
883 super(FakeGithubConnection, self).__init__(driver, connection_name,
884 connection_config)
885 self.connection_name = connection_name
886 self.pr_number = 0
887 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700888 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700889 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100890 self.merge_failure = False
891 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100892 self.reports = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700893
Jesse Keatinga41566f2017-06-14 18:17:51 -0700894 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700895 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700896 self.pr_number += 1
897 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100898 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700899 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700900 self.pull_requests.append(pull_request)
901 return pull_request
902
Jesse Keating71a47ff2017-06-06 11:36:43 -0700903 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
904 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700905 if not old_rev:
906 old_rev = '00000000000000000000000000000000'
907 if not new_rev:
908 new_rev = random_sha1()
909 name = 'push'
910 data = {
911 'ref': ref,
912 'before': old_rev,
913 'after': new_rev,
914 'repository': {
915 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700916 },
917 'commits': [
918 {
919 'added': added_files,
920 'removed': removed_files,
921 'modified': modified_files
922 }
923 ]
Wayne1a78c612015-06-11 17:14:13 -0700924 }
925 return (name, data)
926
Gregory Haynes4fc12542015-04-22 20:38:06 -0700927 def emitEvent(self, event):
928 """Emulates sending the GitHub webhook event to the connection."""
929 port = self.webapp.server.socket.getsockname()[1]
930 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700931 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700932 headers = {'X-Github-Event': name}
933 req = urllib.request.Request(
934 'http://localhost:%s/connection/%s/payload'
935 % (port, self.connection_name),
936 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000937 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700938
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200939 def getPull(self, project, number):
940 pr = self.pull_requests[number - 1]
941 data = {
942 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100943 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200944 'updated_at': pr.updated_at,
945 'base': {
946 'repo': {
947 'full_name': pr.project
948 },
949 'ref': pr.branch,
950 },
Jan Hruban37615e52015-11-19 14:30:49 +0100951 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700952 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200953 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800954 'sha': pr.head_sha,
955 'repo': {
956 'full_name': pr.project
957 }
Jesse Keating61040e72017-06-08 15:08:27 -0700958 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700959 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700960 'labels': pr.labels,
961 'merged': pr.is_merged,
962 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200963 }
964 return data
965
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800966 def getPullBySha(self, sha):
967 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
968 if len(prs) > 1:
969 raise Exception('Multiple pulls found with head sha: %s' % sha)
970 pr = prs[0]
971 return self.getPull(pr.project, pr.number)
972
Jesse Keatingae4cd272017-01-30 17:10:44 -0800973 def _getPullReviews(self, owner, project, number):
974 pr = self.pull_requests[number - 1]
975 return pr.reviews
976
Jan Hruban3b415922016-02-03 13:10:22 +0100977 def getUser(self, login):
978 data = {
979 'username': login,
980 'name': 'Github User',
981 'email': 'github.user@example.com'
982 }
983 return data
984
Jesse Keatingae4cd272017-01-30 17:10:44 -0800985 def getRepoPermission(self, project, login):
986 owner, proj = project.split('/')
987 for pr in self.pull_requests:
988 pr_owner, pr_project = pr.project.split('/')
989 if (pr_owner == owner and proj == pr_project):
990 if login in pr.writers:
991 return 'write'
992 else:
993 return 'read'
994
Gregory Haynes4fc12542015-04-22 20:38:06 -0700995 def getGitUrl(self, project):
996 return os.path.join(self.upstream_root, str(project))
997
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200998 def real_getGitUrl(self, project):
999 return super(FakeGithubConnection, self).getGitUrl(project)
1000
Gregory Haynes4fc12542015-04-22 20:38:06 -07001001 def getProjectBranches(self, project):
1002 """Masks getProjectBranches since we don't have a real github"""
1003
1004 # just returns master for now
1005 return ['master']
1006
Jan Hrubane252a732017-01-03 15:03:09 +01001007 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001008 # record that this got reported
1009 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001010 pull_request = self.pull_requests[pr_number - 1]
1011 pull_request.addComment(message)
1012
Jan Hruban3b415922016-02-03 13:10:22 +01001013 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001014 # record that this got reported
1015 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001016 pull_request = self.pull_requests[pr_number - 1]
1017 if self.merge_failure:
1018 raise Exception('Pull request was not merged')
1019 if self.merge_not_allowed_count > 0:
1020 self.merge_not_allowed_count -= 1
1021 raise MergeFailure('Merge was not successful due to mergeability'
1022 ' conflict')
1023 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001024 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001025
Jesse Keatingd96e5882017-01-19 13:55:50 -08001026 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001027 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001028
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001029 def setCommitStatus(self, project, sha, state, url='', description='',
1030 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001031 # record that this got reported
1032 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001033 # always insert a status to the front of the list, to represent
1034 # the last status provided for a commit.
1035 # Since we're bypassing github API, which would require a user, we
1036 # default the user as 'zuul' here.
1037 self.statuses.setdefault(project, {}).setdefault(sha, [])
1038 self.statuses[project][sha].insert(0, {
1039 'state': state,
1040 'url': url,
1041 'description': description,
1042 'context': context,
1043 'creator': {
1044 'login': user
1045 }
1046 })
Jan Hrubane252a732017-01-03 15:03:09 +01001047
Jan Hruban16ad31f2015-11-07 14:39:07 +01001048 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001049 # record that this got reported
1050 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001051 pull_request = self.pull_requests[pr_number - 1]
1052 pull_request.addLabel(label)
1053
1054 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001055 # record that this got reported
1056 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001057 pull_request = self.pull_requests[pr_number - 1]
1058 pull_request.removeLabel(label)
1059
Jesse Keatinga41566f2017-06-14 18:17:51 -07001060 def _getNeededByFromPR(self, change):
1061 prs = []
1062 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001063 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001064 change.number))
1065 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001066 if not pr.body:
1067 body = ''
1068 else:
1069 body = pr.body
1070 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001071 # Get our version of a pull so that it's a dict
1072 pull = self.getPull(pr.project, pr.number)
1073 prs.append(pull)
1074
1075 return prs
1076
Gregory Haynes4fc12542015-04-22 20:38:06 -07001077
Clark Boylanb640e052014-04-03 16:41:46 -07001078class BuildHistory(object):
1079 def __init__(self, **kw):
1080 self.__dict__.update(kw)
1081
1082 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001083 return ("<Completed build, result: %s name: %s uuid: %s "
1084 "changes: %s ref: %s>" %
1085 (self.result, self.name, self.uuid,
1086 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001087
1088
Clark Boylanb640e052014-04-03 16:41:46 -07001089class FakeStatsd(threading.Thread):
1090 def __init__(self):
1091 threading.Thread.__init__(self)
1092 self.daemon = True
1093 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1094 self.sock.bind(('', 0))
1095 self.port = self.sock.getsockname()[1]
1096 self.wake_read, self.wake_write = os.pipe()
1097 self.stats = []
1098
1099 def run(self):
1100 while True:
1101 poll = select.poll()
1102 poll.register(self.sock, select.POLLIN)
1103 poll.register(self.wake_read, select.POLLIN)
1104 ret = poll.poll()
1105 for (fd, event) in ret:
1106 if fd == self.sock.fileno():
1107 data = self.sock.recvfrom(1024)
1108 if not data:
1109 return
1110 self.stats.append(data[0])
1111 if fd == self.wake_read:
1112 return
1113
1114 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001115 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001116
1117
James E. Blaire1767bc2016-08-02 10:00:27 -07001118class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001119 log = logging.getLogger("zuul.test")
1120
Paul Belanger174a8272017-03-14 13:20:10 -04001121 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001123 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001124 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001125 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001126 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001127 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001128 # TODOv3(jeblair): self.node is really "the label of the node
1129 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001130 # keep using it like this, or we may end up exposing more of
1131 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001132 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001133 self.node = None
1134 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001135 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001136 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001137 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001138 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001139 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001140 self.wait_condition = threading.Condition()
1141 self.waiting = False
1142 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001143 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001144 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001145 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001146 items = self.parameters['zuul']['items']
1147 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1148 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001149
James E. Blair3158e282016-08-19 09:34:11 -07001150 def __repr__(self):
1151 waiting = ''
1152 if self.waiting:
1153 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001154 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1155 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001156
Clark Boylanb640e052014-04-03 16:41:46 -07001157 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001158 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001159 self.wait_condition.acquire()
1160 self.wait_condition.notify()
1161 self.waiting = False
1162 self.log.debug("Build %s released" % self.unique)
1163 self.wait_condition.release()
1164
1165 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001166 """Return whether this build is being held.
1167
1168 :returns: Whether the build is being held.
1169 :rtype: bool
1170 """
1171
Clark Boylanb640e052014-04-03 16:41:46 -07001172 self.wait_condition.acquire()
1173 if self.waiting:
1174 ret = True
1175 else:
1176 ret = False
1177 self.wait_condition.release()
1178 return ret
1179
1180 def _wait(self):
1181 self.wait_condition.acquire()
1182 self.waiting = True
1183 self.log.debug("Build %s waiting" % self.unique)
1184 self.wait_condition.wait()
1185 self.wait_condition.release()
1186
1187 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001188 self.log.debug('Running build %s' % self.unique)
1189
Paul Belanger174a8272017-03-14 13:20:10 -04001190 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001191 self.log.debug('Holding build %s' % self.unique)
1192 self._wait()
1193 self.log.debug("Build %s continuing" % self.unique)
1194
James E. Blair412fba82017-01-26 15:00:50 -08001195 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001196 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001197 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001198 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001199 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001200 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001201 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001202
James E. Blaire1767bc2016-08-02 10:00:27 -07001203 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001204
James E. Blaira5dba232016-08-08 15:53:24 -07001205 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001206 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001207 for change in changes:
1208 if self.hasChanges(change):
1209 return True
1210 return False
1211
James E. Blaire7b99a02016-08-05 14:27:34 -07001212 def hasChanges(self, *changes):
1213 """Return whether this build has certain changes in its git repos.
1214
1215 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001216 are expected to be present (in order) in the git repository of
1217 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001218
1219 :returns: Whether the build has the indicated changes.
1220 :rtype: bool
1221
1222 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001223 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001224 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001225 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001226 try:
1227 repo = git.Repo(path)
1228 except NoSuchPathError as e:
1229 self.log.debug('%s' % e)
1230 return False
James E. Blair247cab72017-07-20 16:52:36 -07001231 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001232 commit_message = '%s-1' % change.subject
1233 self.log.debug("Checking if build %s has changes; commit_message "
1234 "%s; repo_messages %s" % (self, commit_message,
1235 repo_messages))
1236 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001237 self.log.debug(" messages do not match")
1238 return False
1239 self.log.debug(" OK")
1240 return True
1241
James E. Blaird8af5422017-05-24 13:59:40 -07001242 def getWorkspaceRepos(self, projects):
1243 """Return workspace git repo objects for the listed projects
1244
1245 :arg list projects: A list of strings, each the canonical name
1246 of a project.
1247
1248 :returns: A dictionary of {name: repo} for every listed
1249 project.
1250 :rtype: dict
1251
1252 """
1253
1254 repos = {}
1255 for project in projects:
1256 path = os.path.join(self.jobdir.src_root, project)
1257 repo = git.Repo(path)
1258 repos[project] = repo
1259 return repos
1260
Clark Boylanb640e052014-04-03 16:41:46 -07001261
Paul Belanger174a8272017-03-14 13:20:10 -04001262class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1263 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001264
Paul Belanger174a8272017-03-14 13:20:10 -04001265 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001266 they will report that they have started but then pause until
1267 released before reporting completion. This attribute may be
1268 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001269 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001270 be explicitly released.
1271
1272 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001273 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001274 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001275 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001276 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001277 self.hold_jobs_in_build = False
1278 self.lock = threading.Lock()
1279 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001280 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001281 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001282 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001283
James E. Blaira5dba232016-08-08 15:53:24 -07001284 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001285 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001286
1287 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001288 :arg Change change: The :py:class:`~tests.base.FakeChange`
1289 instance which should cause the job to fail. This job
1290 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001291
1292 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001293 l = self.fail_tests.get(name, [])
1294 l.append(change)
1295 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001296
James E. Blair962220f2016-08-03 11:22:38 -07001297 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001298 """Release a held build.
1299
1300 :arg str regex: A regular expression which, if supplied, will
1301 cause only builds with matching names to be released. If
1302 not supplied, all builds will be released.
1303
1304 """
James E. Blair962220f2016-08-03 11:22:38 -07001305 builds = self.running_builds[:]
1306 self.log.debug("Releasing build %s (%s)" % (regex,
1307 len(self.running_builds)))
1308 for build in builds:
1309 if not regex or re.match(regex, build.name):
1310 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001311 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001312 build.release()
1313 else:
1314 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001315 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001316 self.log.debug("Done releasing builds %s (%s)" %
1317 (regex, len(self.running_builds)))
1318
Paul Belanger174a8272017-03-14 13:20:10 -04001319 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001320 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001321 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001322 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001323 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001324 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001325 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001326 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001327 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1328 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001329
1330 def stopJob(self, job):
1331 self.log.debug("handle stop")
1332 parameters = json.loads(job.arguments)
1333 uuid = parameters['uuid']
1334 for build in self.running_builds:
1335 if build.unique == uuid:
1336 build.aborted = True
1337 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001338 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001339
James E. Blaira002b032017-04-18 10:35:48 -07001340 def stop(self):
1341 for build in self.running_builds:
1342 build.release()
1343 super(RecordingExecutorServer, self).stop()
1344
Joshua Hesketh50c21782016-10-13 21:34:14 +11001345
Paul Belanger174a8272017-03-14 13:20:10 -04001346class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001347 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001348 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001349 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001350 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001351 if not commit: # merge conflict
1352 self.recordResult('MERGER_FAILURE')
1353 return commit
1354
1355 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001356 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001357 self.executor_server.lock.acquire()
1358 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001359 BuildHistory(name=build.name, result=result, changes=build.changes,
1360 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001361 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001362 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001363 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001364 )
Paul Belanger174a8272017-03-14 13:20:10 -04001365 self.executor_server.running_builds.remove(build)
1366 del self.executor_server.job_builds[self.job.unique]
1367 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001368
1369 def runPlaybooks(self, args):
1370 build = self.executor_server.job_builds[self.job.unique]
1371 build.jobdir = self.jobdir
1372
1373 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1374 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001375 return result
1376
James E. Blair74a82cf2017-07-12 17:23:08 -07001377 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001378 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001379
Paul Belanger174a8272017-03-14 13:20:10 -04001380 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001381 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001382 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001383 else:
1384 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001385 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001386
James E. Blairad8dca02017-02-21 11:48:32 -05001387 def getHostList(self, args):
1388 self.log.debug("hostlist")
1389 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001390 for host in hosts:
1391 host['host_vars']['ansible_connection'] = 'local'
1392
1393 hosts.append(dict(
1394 name='localhost',
1395 host_vars=dict(ansible_connection='local'),
1396 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001397 return hosts
1398
James E. Blairf5dbd002015-12-23 15:26:17 -08001399
Clark Boylanb640e052014-04-03 16:41:46 -07001400class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001401 """A Gearman server for use in tests.
1402
1403 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1404 added to the queue but will not be distributed to workers
1405 until released. This attribute may be changed at any time and
1406 will take effect for subsequently enqueued jobs, but
1407 previously held jobs will still need to be explicitly
1408 released.
1409
1410 """
1411
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001412 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001413 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001414 if use_ssl:
1415 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1416 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1417 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1418 else:
1419 ssl_ca = None
1420 ssl_cert = None
1421 ssl_key = None
1422
1423 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1424 ssl_cert=ssl_cert,
1425 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001426
1427 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001428 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1429 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001430 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001431 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001432 job.waiting = self.hold_jobs_in_queue
1433 else:
1434 job.waiting = False
1435 if job.waiting:
1436 continue
1437 if job.name in connection.functions:
1438 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001439 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001440 connection.related_jobs[job.handle] = job
1441 job.worker_connection = connection
1442 job.running = True
1443 return job
1444 return None
1445
1446 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001447 """Release a held job.
1448
1449 :arg str regex: A regular expression which, if supplied, will
1450 cause only jobs with matching names to be released. If
1451 not supplied, all jobs will be released.
1452 """
Clark Boylanb640e052014-04-03 16:41:46 -07001453 released = False
1454 qlen = (len(self.high_queue) + len(self.normal_queue) +
1455 len(self.low_queue))
1456 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1457 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001458 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001459 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001460 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001461 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001462 self.log.debug("releasing queued job %s" %
1463 job.unique)
1464 job.waiting = False
1465 released = True
1466 else:
1467 self.log.debug("not releasing queued job %s" %
1468 job.unique)
1469 if released:
1470 self.wakeConnections()
1471 qlen = (len(self.high_queue) + len(self.normal_queue) +
1472 len(self.low_queue))
1473 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1474
1475
1476class FakeSMTP(object):
1477 log = logging.getLogger('zuul.FakeSMTP')
1478
1479 def __init__(self, messages, server, port):
1480 self.server = server
1481 self.port = port
1482 self.messages = messages
1483
1484 def sendmail(self, from_email, to_email, msg):
1485 self.log.info("Sending email from %s, to %s, with msg %s" % (
1486 from_email, to_email, msg))
1487
1488 headers = msg.split('\n\n', 1)[0]
1489 body = msg.split('\n\n', 1)[1]
1490
1491 self.messages.append(dict(
1492 from_email=from_email,
1493 to_email=to_email,
1494 msg=msg,
1495 headers=headers,
1496 body=body,
1497 ))
1498
1499 return True
1500
1501 def quit(self):
1502 return True
1503
1504
James E. Blairdce6cea2016-12-20 16:45:32 -08001505class FakeNodepool(object):
1506 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001507 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001508
1509 log = logging.getLogger("zuul.test.FakeNodepool")
1510
1511 def __init__(self, host, port, chroot):
1512 self.client = kazoo.client.KazooClient(
1513 hosts='%s:%s%s' % (host, port, chroot))
1514 self.client.start()
1515 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001516 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001517 self.thread = threading.Thread(target=self.run)
1518 self.thread.daemon = True
1519 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001520 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001521
1522 def stop(self):
1523 self._running = False
1524 self.thread.join()
1525 self.client.stop()
1526 self.client.close()
1527
1528 def run(self):
1529 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001530 try:
1531 self._run()
1532 except Exception:
1533 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001534 time.sleep(0.1)
1535
1536 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001537 if self.paused:
1538 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001539 for req in self.getNodeRequests():
1540 self.fulfillRequest(req)
1541
1542 def getNodeRequests(self):
1543 try:
1544 reqids = self.client.get_children(self.REQUEST_ROOT)
1545 except kazoo.exceptions.NoNodeError:
1546 return []
1547 reqs = []
1548 for oid in sorted(reqids):
1549 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001550 try:
1551 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001552 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001553 data['_oid'] = oid
1554 reqs.append(data)
1555 except kazoo.exceptions.NoNodeError:
1556 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001557 return reqs
1558
James E. Blaire18d4602017-01-05 11:17:28 -08001559 def getNodes(self):
1560 try:
1561 nodeids = self.client.get_children(self.NODE_ROOT)
1562 except kazoo.exceptions.NoNodeError:
1563 return []
1564 nodes = []
1565 for oid in sorted(nodeids):
1566 path = self.NODE_ROOT + '/' + oid
1567 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001568 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001569 data['_oid'] = oid
1570 try:
1571 lockfiles = self.client.get_children(path + '/lock')
1572 except kazoo.exceptions.NoNodeError:
1573 lockfiles = []
1574 if lockfiles:
1575 data['_lock'] = True
1576 else:
1577 data['_lock'] = False
1578 nodes.append(data)
1579 return nodes
1580
James E. Blaira38c28e2017-01-04 10:33:20 -08001581 def makeNode(self, request_id, node_type):
1582 now = time.time()
1583 path = '/nodepool/nodes/'
1584 data = dict(type=node_type,
1585 provider='test-provider',
1586 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001587 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001588 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001589 public_ipv4='127.0.0.1',
1590 private_ipv4=None,
1591 public_ipv6=None,
1592 allocated_to=request_id,
1593 state='ready',
1594 state_time=now,
1595 created_time=now,
1596 updated_time=now,
1597 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001598 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001599 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001600 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001601 path = self.client.create(path, data,
1602 makepath=True,
1603 sequence=True)
1604 nodeid = path.split("/")[-1]
1605 return nodeid
1606
James E. Blair6ab79e02017-01-06 10:10:17 -08001607 def addFailRequest(self, request):
1608 self.fail_requests.add(request['_oid'])
1609
James E. Blairdce6cea2016-12-20 16:45:32 -08001610 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001611 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001612 return
1613 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001614 oid = request['_oid']
1615 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001616
James E. Blair6ab79e02017-01-06 10:10:17 -08001617 if oid in self.fail_requests:
1618 request['state'] = 'failed'
1619 else:
1620 request['state'] = 'fulfilled'
1621 nodes = []
1622 for node in request['node_types']:
1623 nodeid = self.makeNode(oid, node)
1624 nodes.append(nodeid)
1625 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001626
James E. Blaira38c28e2017-01-04 10:33:20 -08001627 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001628 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001629 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001630 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001631 try:
1632 self.client.set(path, data)
1633 except kazoo.exceptions.NoNodeError:
1634 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001635
1636
James E. Blair498059b2016-12-20 13:50:13 -08001637class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001638 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001639 super(ChrootedKazooFixture, self).__init__()
1640
1641 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1642 if ':' in zk_host:
1643 host, port = zk_host.split(':')
1644 else:
1645 host = zk_host
1646 port = None
1647
1648 self.zookeeper_host = host
1649
1650 if not port:
1651 self.zookeeper_port = 2181
1652 else:
1653 self.zookeeper_port = int(port)
1654
Clark Boylan621ec9a2017-04-07 17:41:33 -07001655 self.test_id = test_id
1656
James E. Blair498059b2016-12-20 13:50:13 -08001657 def _setUp(self):
1658 # Make sure the test chroot paths do not conflict
1659 random_bits = ''.join(random.choice(string.ascii_lowercase +
1660 string.ascii_uppercase)
1661 for x in range(8))
1662
Clark Boylan621ec9a2017-04-07 17:41:33 -07001663 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001664 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1665
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001666 self.addCleanup(self._cleanup)
1667
James E. Blair498059b2016-12-20 13:50:13 -08001668 # Ensure the chroot path exists and clean up any pre-existing znodes.
1669 _tmp_client = kazoo.client.KazooClient(
1670 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1671 _tmp_client.start()
1672
1673 if _tmp_client.exists(self.zookeeper_chroot):
1674 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1675
1676 _tmp_client.ensure_path(self.zookeeper_chroot)
1677 _tmp_client.stop()
1678 _tmp_client.close()
1679
James E. Blair498059b2016-12-20 13:50:13 -08001680 def _cleanup(self):
1681 '''Remove the chroot path.'''
1682 # Need a non-chroot'ed client to remove the chroot path
1683 _tmp_client = kazoo.client.KazooClient(
1684 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1685 _tmp_client.start()
1686 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1687 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001688 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001689
1690
Joshua Heskethd78b4482015-09-14 16:56:34 -06001691class MySQLSchemaFixture(fixtures.Fixture):
1692 def setUp(self):
1693 super(MySQLSchemaFixture, self).setUp()
1694
1695 random_bits = ''.join(random.choice(string.ascii_lowercase +
1696 string.ascii_uppercase)
1697 for x in range(8))
1698 self.name = '%s_%s' % (random_bits, os.getpid())
1699 self.passwd = uuid.uuid4().hex
1700 db = pymysql.connect(host="localhost",
1701 user="openstack_citest",
1702 passwd="openstack_citest",
1703 db="openstack_citest")
1704 cur = db.cursor()
1705 cur.execute("create database %s" % self.name)
1706 cur.execute(
1707 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1708 (self.name, self.name, self.passwd))
1709 cur.execute("flush privileges")
1710
1711 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1712 self.passwd,
1713 self.name)
1714 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1715 self.addCleanup(self.cleanup)
1716
1717 def cleanup(self):
1718 db = pymysql.connect(host="localhost",
1719 user="openstack_citest",
1720 passwd="openstack_citest",
1721 db="openstack_citest")
1722 cur = db.cursor()
1723 cur.execute("drop database %s" % self.name)
1724 cur.execute("drop user '%s'@'localhost'" % self.name)
1725 cur.execute("flush privileges")
1726
1727
Maru Newby3fe5f852015-01-13 04:22:14 +00001728class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001729 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001730 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001731
James E. Blair1c236df2017-02-01 14:07:24 -08001732 def attachLogs(self, *args):
1733 def reader():
1734 self._log_stream.seek(0)
1735 while True:
1736 x = self._log_stream.read(4096)
1737 if not x:
1738 break
1739 yield x.encode('utf8')
1740 content = testtools.content.content_from_reader(
1741 reader,
1742 testtools.content_type.UTF8_TEXT,
1743 False)
1744 self.addDetail('logging', content)
1745
Clark Boylanb640e052014-04-03 16:41:46 -07001746 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001747 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001748 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1749 try:
1750 test_timeout = int(test_timeout)
1751 except ValueError:
1752 # If timeout value is invalid do not set a timeout.
1753 test_timeout = 0
1754 if test_timeout > 0:
1755 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1756
1757 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1758 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1759 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1760 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1761 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1762 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1763 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1764 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1765 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1766 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001767 self._log_stream = StringIO()
1768 self.addOnException(self.attachLogs)
1769 else:
1770 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001771
James E. Blair73b41772017-05-22 13:22:55 -07001772 # NOTE(jeblair): this is temporary extra debugging to try to
1773 # track down a possible leak.
1774 orig_git_repo_init = git.Repo.__init__
1775
1776 def git_repo_init(myself, *args, **kw):
1777 orig_git_repo_init(myself, *args, **kw)
1778 self.log.debug("Created git repo 0x%x %s" %
1779 (id(myself), repr(myself)))
1780
1781 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1782 git_repo_init))
1783
James E. Blair1c236df2017-02-01 14:07:24 -08001784 handler = logging.StreamHandler(self._log_stream)
1785 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1786 '%(levelname)-8s %(message)s')
1787 handler.setFormatter(formatter)
1788
1789 logger = logging.getLogger()
1790 logger.setLevel(logging.DEBUG)
1791 logger.addHandler(handler)
1792
Clark Boylan3410d532017-04-25 12:35:29 -07001793 # Make sure we don't carry old handlers around in process state
1794 # which slows down test runs
1795 self.addCleanup(logger.removeHandler, handler)
1796 self.addCleanup(handler.close)
1797 self.addCleanup(handler.flush)
1798
James E. Blair1c236df2017-02-01 14:07:24 -08001799 # NOTE(notmorgan): Extract logging overrides for specific
1800 # libraries from the OS_LOG_DEFAULTS env and create loggers
1801 # for each. This is used to limit the output during test runs
1802 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001803 log_defaults_from_env = os.environ.get(
1804 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001805 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001806
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 if log_defaults_from_env:
1808 for default in log_defaults_from_env.split(','):
1809 try:
1810 name, level_str = default.split('=', 1)
1811 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001812 logger = logging.getLogger(name)
1813 logger.setLevel(level)
1814 logger.addHandler(handler)
1815 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001816 except ValueError:
1817 # NOTE(notmorgan): Invalid format of the log default,
1818 # skip and don't try and apply a logger for the
1819 # specified module
1820 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001821
Maru Newby3fe5f852015-01-13 04:22:14 +00001822
1823class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001824 """A test case with a functioning Zuul.
1825
1826 The following class variables are used during test setup and can
1827 be overidden by subclasses but are effectively read-only once a
1828 test method starts running:
1829
1830 :cvar str config_file: This points to the main zuul config file
1831 within the fixtures directory. Subclasses may override this
1832 to obtain a different behavior.
1833
1834 :cvar str tenant_config_file: This is the tenant config file
1835 (which specifies from what git repos the configuration should
1836 be loaded). It defaults to the value specified in
1837 `config_file` but can be overidden by subclasses to obtain a
1838 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001839 configuration. See also the :py:func:`simple_layout`
1840 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001841
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001842 :cvar bool create_project_keys: Indicates whether Zuul should
1843 auto-generate keys for each project, or whether the test
1844 infrastructure should insert dummy keys to save time during
1845 startup. Defaults to False.
1846
James E. Blaire7b99a02016-08-05 14:27:34 -07001847 The following are instance variables that are useful within test
1848 methods:
1849
1850 :ivar FakeGerritConnection fake_<connection>:
1851 A :py:class:`~tests.base.FakeGerritConnection` will be
1852 instantiated for each connection present in the config file
1853 and stored here. For instance, `fake_gerrit` will hold the
1854 FakeGerritConnection object for a connection named `gerrit`.
1855
1856 :ivar FakeGearmanServer gearman_server: An instance of
1857 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1858 server that all of the Zuul components in this test use to
1859 communicate with each other.
1860
Paul Belanger174a8272017-03-14 13:20:10 -04001861 :ivar RecordingExecutorServer executor_server: An instance of
1862 :py:class:`~tests.base.RecordingExecutorServer` which is the
1863 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001864
1865 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1866 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001867 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001868 list upon completion.
1869
1870 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1871 objects representing completed builds. They are appended to
1872 the list in the order they complete.
1873
1874 """
1875
James E. Blair83005782015-12-11 14:46:03 -08001876 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001877 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001878 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001879 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001880
1881 def _startMerger(self):
1882 self.merge_server = zuul.merger.server.MergeServer(self.config,
1883 self.connections)
1884 self.merge_server.start()
1885
Maru Newby3fe5f852015-01-13 04:22:14 +00001886 def setUp(self):
1887 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001888
1889 self.setupZK()
1890
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001891 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001892 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001893 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1894 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001895 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001896 tmp_root = tempfile.mkdtemp(
1897 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001898 self.test_root = os.path.join(tmp_root, "zuul-test")
1899 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001900 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001901 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001902 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001903 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1904 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001905
1906 if os.path.exists(self.test_root):
1907 shutil.rmtree(self.test_root)
1908 os.makedirs(self.test_root)
1909 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001910 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001911 os.makedirs(self.merger_state_root)
1912 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001913
1914 # Make per test copy of Configuration.
1915 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001916 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1917 if not os.path.exists(self.private_key_file):
1918 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1919 shutil.copy(src_private_key_file, self.private_key_file)
1920 shutil.copy('{}.pub'.format(src_private_key_file),
1921 '{}.pub'.format(self.private_key_file))
1922 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001923 self.config.set('scheduler', 'tenant_config',
1924 os.path.join(
1925 FIXTURE_DIR,
1926 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001927 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001928 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001929 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001930 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001931 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001932 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001933
Clark Boylanb640e052014-04-03 16:41:46 -07001934 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001935 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1936 # see: https://github.com/jsocol/pystatsd/issues/61
1937 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001938 os.environ['STATSD_PORT'] = str(self.statsd.port)
1939 self.statsd.start()
1940 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001941 importlib.reload(statsd)
1942 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001943
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001944 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001945
1946 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001947 self.log.info("Gearman server on port %s" %
1948 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001949 if self.use_ssl:
1950 self.log.info('SSL enabled for gearman')
1951 self.config.set(
1952 'gearman', 'ssl_ca',
1953 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1954 self.config.set(
1955 'gearman', 'ssl_cert',
1956 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1957 self.config.set(
1958 'gearman', 'ssl_key',
1959 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001960
James E. Blaire511d2f2016-12-08 15:22:26 -08001961 gerritsource.GerritSource.replication_timeout = 1.5
1962 gerritsource.GerritSource.replication_retry_interval = 0.5
1963 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001964
Joshua Hesketh352264b2015-08-11 23:42:08 +10001965 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001966
Jan Hruban7083edd2015-08-21 14:00:54 +02001967 self.webapp = zuul.webapp.WebApp(
1968 self.sched, port=0, listen_address='127.0.0.1')
1969
Jan Hruban6b71aff2015-10-22 16:58:08 +02001970 self.event_queues = [
1971 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001972 self.sched.trigger_event_queue,
1973 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001974 ]
1975
James E. Blairfef78942016-03-11 16:28:56 -08001976 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001977 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001978
Paul Belanger174a8272017-03-14 13:20:10 -04001979 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001980 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001981 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001982 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001983 _test_root=self.test_root,
1984 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001985 self.executor_server.start()
1986 self.history = self.executor_server.build_history
1987 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001988
Paul Belanger174a8272017-03-14 13:20:10 -04001989 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001990 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001991 self.merge_client = zuul.merger.client.MergeClient(
1992 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001993 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001994 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001995 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001996
James E. Blair0d5a36e2017-02-21 10:53:44 -05001997 self.fake_nodepool = FakeNodepool(
1998 self.zk_chroot_fixture.zookeeper_host,
1999 self.zk_chroot_fixture.zookeeper_port,
2000 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002001
Paul Belanger174a8272017-03-14 13:20:10 -04002002 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002003 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002004 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002005 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002006
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002007 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002008
2009 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002010 self.webapp.start()
2011 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002012 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002013 # Cleanups are run in reverse order
2014 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002015 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002016 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002017
James E. Blairb9c0d772017-03-03 14:34:49 -08002018 self.sched.reconfigure(self.config)
2019 self.sched.resume()
2020
Tobias Henkel7df274b2017-05-26 17:41:11 +02002021 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002022 # Set up gerrit related fakes
2023 # Set a changes database so multiple FakeGerrit's can report back to
2024 # a virtual canonical database given by the configured hostname
2025 self.gerrit_changes_dbs = {}
2026
2027 def getGerritConnection(driver, name, config):
2028 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2029 con = FakeGerritConnection(driver, name, config,
2030 changes_db=db,
2031 upstream_root=self.upstream_root)
2032 self.event_queues.append(con.event_queue)
2033 setattr(self, 'fake_' + name, con)
2034 return con
2035
2036 self.useFixture(fixtures.MonkeyPatch(
2037 'zuul.driver.gerrit.GerritDriver.getConnection',
2038 getGerritConnection))
2039
Gregory Haynes4fc12542015-04-22 20:38:06 -07002040 def getGithubConnection(driver, name, config):
2041 con = FakeGithubConnection(driver, name, config,
2042 upstream_root=self.upstream_root)
2043 setattr(self, 'fake_' + name, con)
2044 return con
2045
2046 self.useFixture(fixtures.MonkeyPatch(
2047 'zuul.driver.github.GithubDriver.getConnection',
2048 getGithubConnection))
2049
James E. Blaire511d2f2016-12-08 15:22:26 -08002050 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002051 # TODO(jhesketh): This should come from lib.connections for better
2052 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002053 # Register connections from the config
2054 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002055
Joshua Hesketh352264b2015-08-11 23:42:08 +10002056 def FakeSMTPFactory(*args, **kw):
2057 args = [self.smtp_messages] + list(args)
2058 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002059
Joshua Hesketh352264b2015-08-11 23:42:08 +10002060 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002061
James E. Blaire511d2f2016-12-08 15:22:26 -08002062 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002063 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002064 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002065
James E. Blair83005782015-12-11 14:46:03 -08002066 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002067 # This creates the per-test configuration object. It can be
2068 # overriden by subclasses, but should not need to be since it
2069 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002070 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002071 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002072
James E. Blair39840362017-06-23 20:34:02 +01002073 sections = ['zuul', 'scheduler', 'executor', 'merger']
2074 for section in sections:
2075 if not self.config.has_section(section):
2076 self.config.add_section(section)
2077
James E. Blair06cc3922017-04-19 10:08:10 -07002078 if not self.setupSimpleLayout():
2079 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002080 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002081 self.tenant_config_file)
2082 git_path = os.path.join(
2083 os.path.dirname(
2084 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2085 'git')
2086 if os.path.exists(git_path):
2087 for reponame in os.listdir(git_path):
2088 project = reponame.replace('_', '/')
2089 self.copyDirToRepo(project,
2090 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002091 # Make test_root persist after ansible run for .flag test
2092 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002093 self.setupAllProjectKeys()
2094
James E. Blair06cc3922017-04-19 10:08:10 -07002095 def setupSimpleLayout(self):
2096 # If the test method has been decorated with a simple_layout,
2097 # use that instead of the class tenant_config_file. Set up a
2098 # single config-project with the specified layout, and
2099 # initialize repos for all of the 'project' entries which
2100 # appear in the layout.
2101 test_name = self.id().split('.')[-1]
2102 test = getattr(self, test_name)
2103 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002104 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002105 else:
2106 return False
2107
James E. Blairb70e55a2017-04-19 12:57:02 -07002108 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002109 path = os.path.join(FIXTURE_DIR, path)
2110 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002111 data = f.read()
2112 layout = yaml.safe_load(data)
2113 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002114 untrusted_projects = []
2115 for item in layout:
2116 if 'project' in item:
2117 name = item['project']['name']
2118 untrusted_projects.append(name)
2119 self.init_repo(name)
2120 self.addCommitToRepo(name, 'initial commit',
2121 files={'README': ''},
2122 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002123 if 'job' in item:
2124 jobname = item['job']['name']
2125 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002126
2127 root = os.path.join(self.test_root, "config")
2128 if not os.path.exists(root):
2129 os.makedirs(root)
2130 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2131 config = [{'tenant':
2132 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002133 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002134 {'config-projects': ['common-config'],
2135 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002136 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002137 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002138 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002139 os.path.join(FIXTURE_DIR, f.name))
2140
2141 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002142 self.addCommitToRepo('common-config', 'add content from fixture',
2143 files, branch='master', tag='init')
2144
2145 return True
2146
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002147 def setupAllProjectKeys(self):
2148 if self.create_project_keys:
2149 return
2150
James E. Blair39840362017-06-23 20:34:02 +01002151 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002152 with open(os.path.join(FIXTURE_DIR, path)) as f:
2153 tenant_config = yaml.safe_load(f.read())
2154 for tenant in tenant_config:
2155 sources = tenant['tenant']['source']
2156 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002157 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002158 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002159 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002160 self.setupProjectKeys(source, project)
2161
2162 def setupProjectKeys(self, source, project):
2163 # Make sure we set up an RSA key for the project so that we
2164 # don't spend time generating one:
2165
James E. Blair6459db12017-06-29 14:57:20 -07002166 if isinstance(project, dict):
2167 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002168 key_root = os.path.join(self.state_root, 'keys')
2169 if not os.path.isdir(key_root):
2170 os.mkdir(key_root, 0o700)
2171 private_key_file = os.path.join(key_root, source, project + '.pem')
2172 private_key_dir = os.path.dirname(private_key_file)
2173 self.log.debug("Installing test keys for project %s at %s" % (
2174 project, private_key_file))
2175 if not os.path.isdir(private_key_dir):
2176 os.makedirs(private_key_dir)
2177 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2178 with open(private_key_file, 'w') as o:
2179 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002180
James E. Blair498059b2016-12-20 13:50:13 -08002181 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002182 self.zk_chroot_fixture = self.useFixture(
2183 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002184 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002185 self.zk_chroot_fixture.zookeeper_host,
2186 self.zk_chroot_fixture.zookeeper_port,
2187 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002188
James E. Blair96c6bf82016-01-15 16:20:40 -08002189 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002190 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002191
2192 files = {}
2193 for (dirpath, dirnames, filenames) in os.walk(source_path):
2194 for filename in filenames:
2195 test_tree_filepath = os.path.join(dirpath, filename)
2196 common_path = os.path.commonprefix([test_tree_filepath,
2197 source_path])
2198 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2199 with open(test_tree_filepath, 'r') as f:
2200 content = f.read()
2201 files[relative_filepath] = content
2202 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002203 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002204
James E. Blaire18d4602017-01-05 11:17:28 -08002205 def assertNodepoolState(self):
2206 # Make sure that there are no pending requests
2207
2208 requests = self.fake_nodepool.getNodeRequests()
2209 self.assertEqual(len(requests), 0)
2210
2211 nodes = self.fake_nodepool.getNodes()
2212 for node in nodes:
2213 self.assertFalse(node['_lock'], "Node %s is locked" %
2214 (node['_oid'],))
2215
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002216 def assertNoGeneratedKeys(self):
2217 # Make sure that Zuul did not generate any project keys
2218 # (unless it was supposed to).
2219
2220 if self.create_project_keys:
2221 return
2222
2223 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2224 test_key = i.read()
2225
2226 key_root = os.path.join(self.state_root, 'keys')
2227 for root, dirname, files in os.walk(key_root):
2228 for fn in files:
2229 with open(os.path.join(root, fn)) as f:
2230 self.assertEqual(test_key, f.read())
2231
Clark Boylanb640e052014-04-03 16:41:46 -07002232 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002233 self.log.debug("Assert final state")
2234 # Make sure no jobs are running
2235 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002236 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002237 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002238 gc.collect()
2239 for obj in gc.get_objects():
2240 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002241 self.log.debug("Leaked git repo object: 0x%x %s" %
2242 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002243 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002244 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002245 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002246 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002247 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002248 for tenant in self.sched.abide.tenants.values():
2249 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002250 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002251 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002252
2253 def shutdown(self):
2254 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002255 self.executor_server.hold_jobs_in_build = False
2256 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002257 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002258 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002259 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002260 self.sched.stop()
2261 self.sched.join()
2262 self.statsd.stop()
2263 self.statsd.join()
2264 self.webapp.stop()
2265 self.webapp.join()
2266 self.rpc.stop()
2267 self.rpc.join()
2268 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002269 self.fake_nodepool.stop()
2270 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002271 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002272 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002273 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002274 # Further the pydevd threads also need to be whitelisted so debugging
2275 # e.g. in PyCharm is possible without breaking shutdown.
2276 whitelist = ['executor-watchdog',
2277 'pydevd.CommandThread',
2278 'pydevd.Reader',
2279 'pydevd.Writer',
2280 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002281 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002282 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002283 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002284 log_str = ""
2285 for thread_id, stack_frame in sys._current_frames().items():
2286 log_str += "Thread: %s\n" % thread_id
2287 log_str += "".join(traceback.format_stack(stack_frame))
2288 self.log.debug(log_str)
2289 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002290
James E. Blaira002b032017-04-18 10:35:48 -07002291 def assertCleanShutdown(self):
2292 pass
2293
James E. Blairc4ba97a2017-04-19 16:26:24 -07002294 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002295 parts = project.split('/')
2296 path = os.path.join(self.upstream_root, *parts[:-1])
2297 if not os.path.exists(path):
2298 os.makedirs(path)
2299 path = os.path.join(self.upstream_root, project)
2300 repo = git.Repo.init(path)
2301
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002302 with repo.config_writer() as config_writer:
2303 config_writer.set_value('user', 'email', 'user@example.com')
2304 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002305
Clark Boylanb640e052014-04-03 16:41:46 -07002306 repo.index.commit('initial commit')
2307 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002308 if tag:
2309 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002310
James E. Blair97d902e2014-08-21 13:25:56 -07002311 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002312 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002313 repo.git.clean('-x', '-f', '-d')
2314
James E. Blair97d902e2014-08-21 13:25:56 -07002315 def create_branch(self, project, branch):
2316 path = os.path.join(self.upstream_root, project)
2317 repo = git.Repo.init(path)
2318 fn = os.path.join(path, 'README')
2319
2320 branch_head = repo.create_head(branch)
2321 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002322 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002323 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002324 f.close()
2325 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002326 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002327
James E. Blair97d902e2014-08-21 13:25:56 -07002328 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002329 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002330 repo.git.clean('-x', '-f', '-d')
2331
Sachi King9f16d522016-03-16 12:20:45 +11002332 def create_commit(self, project):
2333 path = os.path.join(self.upstream_root, project)
2334 repo = git.Repo(path)
2335 repo.head.reference = repo.heads['master']
2336 file_name = os.path.join(path, 'README')
2337 with open(file_name, 'a') as f:
2338 f.write('creating fake commit\n')
2339 repo.index.add([file_name])
2340 commit = repo.index.commit('Creating a fake commit')
2341 return commit.hexsha
2342
James E. Blairf4a5f022017-04-18 14:01:10 -07002343 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002344 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002345 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002346 while len(self.builds):
2347 self.release(self.builds[0])
2348 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002349 i += 1
2350 if count is not None and i >= count:
2351 break
James E. Blairb8c16472015-05-05 14:55:26 -07002352
James E. Blairdf25ddc2017-07-08 07:57:09 -07002353 def getSortedBuilds(self):
2354 "Return the list of currently running builds sorted by name"
2355
2356 return sorted(self.builds, key=lambda x: x.name)
2357
Clark Boylanb640e052014-04-03 16:41:46 -07002358 def release(self, job):
2359 if isinstance(job, FakeBuild):
2360 job.release()
2361 else:
2362 job.waiting = False
2363 self.log.debug("Queued job %s released" % job.unique)
2364 self.gearman_server.wakeConnections()
2365
2366 def getParameter(self, job, name):
2367 if isinstance(job, FakeBuild):
2368 return job.parameters[name]
2369 else:
2370 parameters = json.loads(job.arguments)
2371 return parameters[name]
2372
Clark Boylanb640e052014-04-03 16:41:46 -07002373 def haveAllBuildsReported(self):
2374 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002375 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002376 return False
2377 # Find out if every build that the worker has completed has been
2378 # reported back to Zuul. If it hasn't then that means a Gearman
2379 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002380 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002381 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002382 if not zbuild:
2383 # It has already been reported
2384 continue
2385 # It hasn't been reported yet.
2386 return False
2387 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002388 worker = self.executor_server.executor_worker
2389 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002390 if connection.state == 'GRAB_WAIT':
2391 return False
2392 return True
2393
2394 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002395 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002396 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002397 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002398 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002399 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002400 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002401 for j in conn.related_jobs.values():
2402 if j.unique == build.uuid:
2403 client_job = j
2404 break
2405 if not client_job:
2406 self.log.debug("%s is not known to the gearman client" %
2407 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002408 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002409 if not client_job.handle:
2410 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002411 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002412 server_job = self.gearman_server.jobs.get(client_job.handle)
2413 if not server_job:
2414 self.log.debug("%s is not known to the gearman server" %
2415 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002416 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002417 if not hasattr(server_job, 'waiting'):
2418 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002419 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002420 if server_job.waiting:
2421 continue
James E. Blair17302972016-08-10 16:11:42 -07002422 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002423 self.log.debug("%s has not reported start" % build)
2424 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002425 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002426 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002427 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002428 if worker_build:
2429 if worker_build.isWaiting():
2430 continue
2431 else:
2432 self.log.debug("%s is running" % worker_build)
2433 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002434 else:
James E. Blair962220f2016-08-03 11:22:38 -07002435 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002436 return False
James E. Blaira002b032017-04-18 10:35:48 -07002437 for (build_uuid, job_worker) in \
2438 self.executor_server.job_workers.items():
2439 if build_uuid not in seen_builds:
2440 self.log.debug("%s is not finalized" % build_uuid)
2441 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002442 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002443
James E. Blairdce6cea2016-12-20 16:45:32 -08002444 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002445 if self.fake_nodepool.paused:
2446 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002447 if self.sched.nodepool.requests:
2448 return False
2449 return True
2450
Jan Hruban6b71aff2015-10-22 16:58:08 +02002451 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002452 for event_queue in self.event_queues:
2453 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002454
2455 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002456 for event_queue in self.event_queues:
2457 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002458
Clark Boylanb640e052014-04-03 16:41:46 -07002459 def waitUntilSettled(self):
2460 self.log.debug("Waiting until settled...")
2461 start = time.time()
2462 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002463 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002464 self.log.error("Timeout waiting for Zuul to settle")
2465 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002466 for event_queue in self.event_queues:
2467 self.log.error(" %s: %s" %
2468 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002469 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002470 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002471 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002472 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002473 self.log.error("All requests completed: %s" %
2474 (self.areAllNodeRequestsComplete(),))
2475 self.log.error("Merge client jobs: %s" %
2476 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002477 raise Exception("Timeout waiting for Zuul to settle")
2478 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002479
Paul Belanger174a8272017-03-14 13:20:10 -04002480 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002481 # have all build states propogated to zuul?
2482 if self.haveAllBuildsReported():
2483 # Join ensures that the queue is empty _and_ events have been
2484 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002485 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002486 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002487 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002488 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002489 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002490 self.areAllNodeRequestsComplete() and
2491 all(self.eventQueuesEmpty())):
2492 # The queue empty check is placed at the end to
2493 # ensure that if a component adds an event between
2494 # when locked the run handler and checked that the
2495 # components were stable, we don't erroneously
2496 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002497 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002498 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002499 self.log.debug("...settled.")
2500 return
2501 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002502 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002503 self.sched.wake_event.wait(0.1)
2504
2505 def countJobResults(self, jobs, result):
2506 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002507 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002508
Monty Taylor0d926122017-05-24 08:07:56 -05002509 def getBuildByName(self, name):
2510 for build in self.builds:
2511 if build.name == name:
2512 return build
2513 raise Exception("Unable to find build %s" % name)
2514
James E. Blair96c6bf82016-01-15 16:20:40 -08002515 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002516 for job in self.history:
2517 if (job.name == name and
2518 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002519 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002520 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002521 raise Exception("Unable to find job %s in history" % name)
2522
2523 def assertEmptyQueues(self):
2524 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002525 for tenant in self.sched.abide.tenants.values():
2526 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002527 for pipeline_queue in pipeline.queues:
2528 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002529 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002530 pipeline.name, pipeline_queue.name,
2531 pipeline_queue.queue))
2532 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002533 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002534
2535 def assertReportedStat(self, key, value=None, kind=None):
2536 start = time.time()
2537 while time.time() < (start + 5):
2538 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002539 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002540 if key == k:
2541 if value is None and kind is None:
2542 return
2543 elif value:
2544 if value == v:
2545 return
2546 elif kind:
2547 if v.endswith('|' + kind):
2548 return
2549 time.sleep(0.1)
2550
Clark Boylanb640e052014-04-03 16:41:46 -07002551 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002552
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002553 def assertBuilds(self, builds):
2554 """Assert that the running builds are as described.
2555
2556 The list of running builds is examined and must match exactly
2557 the list of builds described by the input.
2558
2559 :arg list builds: A list of dictionaries. Each item in the
2560 list must match the corresponding build in the build
2561 history, and each element of the dictionary must match the
2562 corresponding attribute of the build.
2563
2564 """
James E. Blair3158e282016-08-19 09:34:11 -07002565 try:
2566 self.assertEqual(len(self.builds), len(builds))
2567 for i, d in enumerate(builds):
2568 for k, v in d.items():
2569 self.assertEqual(
2570 getattr(self.builds[i], k), v,
2571 "Element %i in builds does not match" % (i,))
2572 except Exception:
2573 for build in self.builds:
2574 self.log.error("Running build: %s" % build)
2575 else:
2576 self.log.error("No running builds")
2577 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002578
James E. Blairb536ecc2016-08-31 10:11:42 -07002579 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002580 """Assert that the completed builds are as described.
2581
2582 The list of completed builds is examined and must match
2583 exactly the list of builds described by the input.
2584
2585 :arg list history: A list of dictionaries. Each item in the
2586 list must match the corresponding build in the build
2587 history, and each element of the dictionary must match the
2588 corresponding attribute of the build.
2589
James E. Blairb536ecc2016-08-31 10:11:42 -07002590 :arg bool ordered: If true, the history must match the order
2591 supplied, if false, the builds are permitted to have
2592 arrived in any order.
2593
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002594 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002595 def matches(history_item, item):
2596 for k, v in item.items():
2597 if getattr(history_item, k) != v:
2598 return False
2599 return True
James E. Blair3158e282016-08-19 09:34:11 -07002600 try:
2601 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002602 if ordered:
2603 for i, d in enumerate(history):
2604 if not matches(self.history[i], d):
2605 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002606 "Element %i in history does not match %s" %
2607 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002608 else:
2609 unseen = self.history[:]
2610 for i, d in enumerate(history):
2611 found = False
2612 for unseen_item in unseen:
2613 if matches(unseen_item, d):
2614 found = True
2615 unseen.remove(unseen_item)
2616 break
2617 if not found:
2618 raise Exception("No match found for element %i "
2619 "in history" % (i,))
2620 if unseen:
2621 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002622 except Exception:
2623 for build in self.history:
2624 self.log.error("Completed build: %s" % build)
2625 else:
2626 self.log.error("No completed builds")
2627 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002628
James E. Blair6ac368c2016-12-22 18:07:20 -08002629 def printHistory(self):
2630 """Log the build history.
2631
2632 This can be useful during tests to summarize what jobs have
2633 completed.
2634
2635 """
2636 self.log.debug("Build history:")
2637 for build in self.history:
2638 self.log.debug(build)
2639
James E. Blair59fdbac2015-12-07 17:08:06 -08002640 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002641 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2642
James E. Blair9ea70072017-04-19 16:05:30 -07002643 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002644 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002645 if not os.path.exists(root):
2646 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002647 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2648 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002649- tenant:
2650 name: openstack
2651 source:
2652 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002653 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002654 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002655 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002656 - org/project
2657 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002658 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002659 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002660 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002661 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002662 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002663
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002664 def addCommitToRepo(self, project, message, files,
2665 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002666 path = os.path.join(self.upstream_root, project)
2667 repo = git.Repo(path)
2668 repo.head.reference = branch
2669 zuul.merger.merger.reset_repo_to_head(repo)
2670 for fn, content in files.items():
2671 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002672 try:
2673 os.makedirs(os.path.dirname(fn))
2674 except OSError:
2675 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002676 with open(fn, 'w') as f:
2677 f.write(content)
2678 repo.index.add([fn])
2679 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002680 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002681 repo.heads[branch].commit = commit
2682 repo.head.reference = branch
2683 repo.git.clean('-x', '-f', '-d')
2684 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002685 if tag:
2686 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002687 return before
2688
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002689 def commitConfigUpdate(self, project_name, source_name):
2690 """Commit an update to zuul.yaml
2691
2692 This overwrites the zuul.yaml in the specificed project with
2693 the contents specified.
2694
2695 :arg str project_name: The name of the project containing
2696 zuul.yaml (e.g., common-config)
2697
2698 :arg str source_name: The path to the file (underneath the
2699 test fixture directory) whose contents should be used to
2700 replace zuul.yaml.
2701 """
2702
2703 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002704 files = {}
2705 with open(source_path, 'r') as f:
2706 data = f.read()
2707 layout = yaml.safe_load(data)
2708 files['zuul.yaml'] = data
2709 for item in layout:
2710 if 'job' in item:
2711 jobname = item['job']['name']
2712 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002713 before = self.addCommitToRepo(
2714 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002715 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002716 return before
2717
James E. Blair7fc8daa2016-08-08 15:37:15 -07002718 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002719
James E. Blair7fc8daa2016-08-08 15:37:15 -07002720 """Inject a Fake (Gerrit) event.
2721
2722 This method accepts a JSON-encoded event and simulates Zuul
2723 having received it from Gerrit. It could (and should)
2724 eventually apply to any connection type, but is currently only
2725 used with Gerrit connections. The name of the connection is
2726 used to look up the corresponding server, and the event is
2727 simulated as having been received by all Zuul connections
2728 attached to that server. So if two Gerrit connections in Zuul
2729 are connected to the same Gerrit server, and you invoke this
2730 method specifying the name of one of them, the event will be
2731 received by both.
2732
2733 .. note::
2734
2735 "self.fake_gerrit.addEvent" calls should be migrated to
2736 this method.
2737
2738 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002739 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002740 :arg str event: The JSON-encoded event.
2741
2742 """
2743 specified_conn = self.connections.connections[connection]
2744 for conn in self.connections.connections.values():
2745 if (isinstance(conn, specified_conn.__class__) and
2746 specified_conn.server == conn.server):
2747 conn.addEvent(event)
2748
James E. Blaird8af5422017-05-24 13:59:40 -07002749 def getUpstreamRepos(self, projects):
2750 """Return upstream git repo objects for the listed projects
2751
2752 :arg list projects: A list of strings, each the canonical name
2753 of a project.
2754
2755 :returns: A dictionary of {name: repo} for every listed
2756 project.
2757 :rtype: dict
2758
2759 """
2760
2761 repos = {}
2762 for project in projects:
2763 # FIXME(jeblair): the upstream root does not yet have a
2764 # hostname component; that needs to be added, and this
2765 # line removed:
2766 tmp_project_name = '/'.join(project.split('/')[1:])
2767 path = os.path.join(self.upstream_root, tmp_project_name)
2768 repo = git.Repo(path)
2769 repos[project] = repo
2770 return repos
2771
James E. Blair3f876d52016-07-22 13:07:14 -07002772
2773class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002774 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002775 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002776
Jamie Lennox7655b552017-03-17 12:33:38 +11002777 @contextmanager
2778 def jobLog(self, build):
2779 """Print job logs on assertion errors
2780
2781 This method is a context manager which, if it encounters an
2782 ecxeption, adds the build log to the debug output.
2783
2784 :arg Build build: The build that's being asserted.
2785 """
2786 try:
2787 yield
2788 except Exception:
2789 path = os.path.join(self.test_root, build.uuid,
2790 'work', 'logs', 'job-output.txt')
2791 with open(path) as f:
2792 self.log.debug(f.read())
2793 raise
2794
Joshua Heskethd78b4482015-09-14 16:56:34 -06002795
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002796class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002797 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002798 use_ssl = True
2799
2800
Joshua Heskethd78b4482015-09-14 16:56:34 -06002801class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002802 def setup_config(self):
2803 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002804 for section_name in self.config.sections():
2805 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2806 section_name, re.I)
2807 if not con_match:
2808 continue
2809
2810 if self.config.get(section_name, 'driver') == 'sql':
2811 f = MySQLSchemaFixture()
2812 self.useFixture(f)
2813 if (self.config.get(section_name, 'dburi') ==
2814 '$MYSQL_FIXTURE_DBURI$'):
2815 self.config.set(section_name, 'dburi', f.dburi)