blob: 35c83245445112712ba3d436552f1edb32027f1a [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
Clark Boylanb640e052014-04-03 16:41:46 -0700141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
174 'url': 'https://hostname/%s' % number}
175
176 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
186 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
195 with open(fn, 'w') as f:
196 f.write(content)
197 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700198 else:
199 for fni in range(100):
200 fn = os.path.join(path, str(fni))
201 f = open(fn, 'w')
202 for ci in range(4096):
203 f.write(random.choice(string.printable))
204 f.close()
205 repo.index.add([fn])
206
207 r = repo.index.commit(msg)
208 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700209 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700210 repo.git.clean('-x', '-f', '-d')
211 repo.heads['master'].checkout()
212 return r
213
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700215 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700217 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 data = ("test %s %s %s\n" %
219 (self.branch, self.number, self.latest_patchset))
220 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700221 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700222 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700223 ps_files = [{'file': '/COMMIT_MSG',
224 'type': 'ADDED'},
225 {'file': 'README',
226 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700228 ps_files.append({'file': f, 'type': 'ADDED'})
229 d = {'approvals': [],
230 'createdOn': time.time(),
231 'files': ps_files,
232 'number': str(self.latest_patchset),
233 'ref': 'refs/changes/1/%s/%s' % (self.number,
234 self.latest_patchset),
235 'revision': c.hexsha,
236 'uploader': {'email': 'user@example.com',
237 'name': 'User name',
238 'username': 'user'}}
239 self.data['currentPatchSet'] = d
240 self.patchsets.append(d)
241 self.data['submitRecords'] = self.getSubmitRecords()
242
243 def getPatchsetCreatedEvent(self, patchset):
244 event = {"type": "patchset-created",
245 "change": {"project": self.project,
246 "branch": self.branch,
247 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
248 "number": str(self.number),
249 "subject": self.subject,
250 "owner": {"name": "User Name"},
251 "url": "https://hostname/3"},
252 "patchSet": self.patchsets[patchset - 1],
253 "uploader": {"name": "User Name"}}
254 return event
255
256 def getChangeRestoredEvent(self):
257 event = {"type": "change-restored",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100266 "patchSet": self.patchsets[-1],
267 "reason": ""}
268 return event
269
270 def getChangeAbandonedEvent(self):
271 event = {"type": "change-abandoned",
272 "change": {"project": self.project,
273 "branch": self.branch,
274 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
275 "number": str(self.number),
276 "subject": self.subject,
277 "owner": {"name": "User Name"},
278 "url": "https://hostname/3"},
279 "abandoner": {"name": "User Name"},
280 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700281 "reason": ""}
282 return event
283
284 def getChangeCommentEvent(self, patchset):
285 event = {"type": "comment-added",
286 "change": {"project": self.project,
287 "branch": self.branch,
288 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
289 "number": str(self.number),
290 "subject": self.subject,
291 "owner": {"name": "User Name"},
292 "url": "https://hostname/3"},
293 "patchSet": self.patchsets[patchset - 1],
294 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200295 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700296 "description": "Code-Review",
297 "value": "0"}],
298 "comment": "This is a comment"}
299 return event
300
James E. Blairc2a5ed72017-02-20 14:12:01 -0500301 def getChangeMergedEvent(self):
302 event = {"submitter": {"name": "Jenkins",
303 "username": "jenkins"},
304 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
305 "patchSet": self.patchsets[-1],
306 "change": self.data,
307 "type": "change-merged",
308 "eventCreatedOn": 1487613810}
309 return event
310
James E. Blair8cce42e2016-10-18 08:18:36 -0700311 def getRefUpdatedEvent(self):
312 path = os.path.join(self.upstream_root, self.project)
313 repo = git.Repo(path)
314 oldrev = repo.heads[self.branch].commit.hexsha
315
316 event = {
317 "type": "ref-updated",
318 "submitter": {
319 "name": "User Name",
320 },
321 "refUpdate": {
322 "oldRev": oldrev,
323 "newRev": self.patchsets[-1]['revision'],
324 "refName": self.branch,
325 "project": self.project,
326 }
327 }
328 return event
329
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 def addApproval(self, category, value, username='reviewer_john',
331 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700332 if not granted_on:
333 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000334 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200335 'description': self.categories[category][0],
336 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000337 'value': str(value),
338 'by': {
339 'username': username,
340 'email': username + '@example.com',
341 },
342 'grantedOn': int(granted_on)
343 }
Clark Boylanb640e052014-04-03 16:41:46 -0700344 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200345 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700346 del self.patchsets[-1]['approvals'][i]
347 self.patchsets[-1]['approvals'].append(approval)
348 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000349 'author': {'email': 'author@example.com',
350 'name': 'Patchset Author',
351 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700352 'change': {'branch': self.branch,
353 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
354 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'owner': {'email': 'owner@example.com',
356 'name': 'Change Owner',
357 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700358 'project': self.project,
359 'subject': self.subject,
360 'topic': 'master',
361 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'patchSet': self.patchsets[-1],
364 'type': 'comment-added'}
365 self.data['submitRecords'] = self.getSubmitRecords()
366 return json.loads(json.dumps(event))
367
368 def getSubmitRecords(self):
369 status = {}
370 for cat in self.categories.keys():
371 status[cat] = 0
372
373 for a in self.patchsets[-1]['approvals']:
374 cur = status[a['type']]
375 cat_min, cat_max = self.categories[a['type']][1:]
376 new = int(a['value'])
377 if new == cat_min:
378 cur = new
379 elif abs(new) > abs(cur):
380 cur = new
381 status[a['type']] = cur
382
383 labels = []
384 ok = True
385 for typ, cat in self.categories.items():
386 cur = status[typ]
387 cat_min, cat_max = cat[1:]
388 if cur == cat_min:
389 value = 'REJECT'
390 ok = False
391 elif cur == cat_max:
392 value = 'OK'
393 else:
394 value = 'NEED'
395 ok = False
396 labels.append({'label': cat[0], 'status': value})
397 if ok:
398 return [{'status': 'OK'}]
399 return [{'status': 'NOT_READY',
400 'labels': labels}]
401
402 def setDependsOn(self, other, patchset):
403 self.depends_on_change = other
404 d = {'id': other.data['id'],
405 'number': other.data['number'],
406 'ref': other.patchsets[patchset - 1]['ref']
407 }
408 self.data['dependsOn'] = [d]
409
410 other.needed_by_changes.append(self)
411 needed = other.data.get('neededBy', [])
412 d = {'id': self.data['id'],
413 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700414 'ref': self.patchsets[-1]['ref'],
415 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700416 }
417 needed.append(d)
418 other.data['neededBy'] = needed
419
420 def query(self):
421 self.queried += 1
422 d = self.data.get('dependsOn')
423 if d:
424 d = d[0]
425 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
426 d['isCurrentPatchSet'] = True
427 else:
428 d['isCurrentPatchSet'] = False
429 return json.loads(json.dumps(self.data))
430
431 def setMerged(self):
432 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000433 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700434 return
435 if self.fail_merge:
436 return
437 self.data['status'] = 'MERGED'
438 self.open = False
439
440 path = os.path.join(self.upstream_root, self.project)
441 repo = git.Repo(path)
442 repo.heads[self.branch].commit = \
443 repo.commit(self.patchsets[-1]['revision'])
444
445 def setReported(self):
446 self.reported += 1
447
448
James E. Blaire511d2f2016-12-08 15:22:26 -0800449class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700450 """A Fake Gerrit connection for use in tests.
451
452 This subclasses
453 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
454 ability for tests to add changes to the fake Gerrit it represents.
455 """
456
Joshua Hesketh352264b2015-08-11 23:42:08 +1000457 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000462 connection_config)
463
Monty Taylorb934c1a2017-06-16 19:31:47 -0500464 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700465 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
466 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700468 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200469 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700470
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700471 def addFakeChange(self, project, branch, subject, status='NEW',
472 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700473 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700475 c = FakeGerritChange(self, self.change_number, project, branch,
476 subject, upstream_root=self.upstream_root,
477 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700478 self.changes[self.change_number] = c
479 return c
480
Clark Boylanb640e052014-04-03 16:41:46 -0700481 def review(self, project, changeid, message, action):
482 number, ps = changeid.split(',')
483 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000484
485 # Add the approval back onto the change (ie simulate what gerrit would
486 # do).
487 # Usually when zuul leaves a review it'll create a feedback loop where
488 # zuul's review enters another gerrit event (which is then picked up by
489 # zuul). However, we can't mimic this behaviour (by adding this
490 # approval event into the queue) as it stops jobs from checking what
491 # happens before this event is triggered. If a job needs to see what
492 # happens they can add their own verified event into the queue.
493 # Nevertheless, we can update change with the new review in gerrit.
494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 for cat in action.keys():
496 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000497 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
Clark Boylanb640e052014-04-03 16:41:46 -0700499 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 if 'submit' in action:
502 change.setMerged()
503 if message:
504 change.setReported()
505
506 def query(self, number):
507 change = self.changes.get(int(number))
508 if change:
509 return change.query()
510 return {}
511
James E. Blairc494d542014-08-06 09:23:52 -0700512 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700513 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700514 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800515 if query.startswith('change:'):
516 # Query a specific changeid
517 changeid = query[len('change:'):]
518 l = [change.query() for change in self.changes.values()
519 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700520 elif query.startswith('message:'):
521 # Query the content of a commit message
522 msg = query[len('message:'):].strip()
523 l = [change.query() for change in self.changes.values()
524 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800525 else:
526 # Query all open changes
527 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700528 return l
James E. Blairc494d542014-08-06 09:23:52 -0700529
Joshua Hesketh352264b2015-08-11 23:42:08 +1000530 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700531 pass
532
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200533 def _uploadPack(self, project):
534 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
535 'multi_ack thin-pack side-band side-band-64k ofs-delta '
536 'shallow no-progress include-tag multi_ack_detailed no-done\n')
537 path = os.path.join(self.upstream_root, project.name)
538 repo = git.Repo(path)
539 for ref in repo.refs:
540 r = ref.object.hexsha + ' ' + ref.path + '\n'
541 ret += '%04x%s' % (len(r) + 4, r)
542 ret += '0000'
543 return ret
544
Joshua Hesketh352264b2015-08-11 23:42:08 +1000545 def getGitUrl(self, project):
546 return os.path.join(self.upstream_root, project.name)
547
Clark Boylanb640e052014-04-03 16:41:46 -0700548
Gregory Haynes4fc12542015-04-22 20:38:06 -0700549class GithubChangeReference(git.Reference):
550 _common_path_default = "refs/pull"
551 _points_to_commits_only = True
552
553
554class FakeGithubPullRequest(object):
555
556 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800557 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700558 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700559 """Creates a new PR with several commits.
560 Sends an event about opened PR."""
561 self.github = github
562 self.source = github
563 self.number = number
564 self.project = project
565 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100566 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700567 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100568 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100572 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100573 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800574 self.reviews = []
575 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self.updated_at = None
577 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100578 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100579 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700580 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100582 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700583 self._updateTimeStamp()
584
Jan Hruban570d01c2016-03-10 21:51:32 +0100585 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700586 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100587 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700588 self._updateTimeStamp()
589
Jan Hruban570d01c2016-03-10 21:51:32 +0100590 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700591 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100592 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700593 self._updateTimeStamp()
594
595 def getPullRequestOpenedEvent(self):
596 return self._getPullRequestEvent('opened')
597
598 def getPullRequestSynchronizeEvent(self):
599 return self._getPullRequestEvent('synchronize')
600
601 def getPullRequestReopenedEvent(self):
602 return self._getPullRequestEvent('reopened')
603
604 def getPullRequestClosedEvent(self):
605 return self._getPullRequestEvent('closed')
606
Jesse Keatinga41566f2017-06-14 18:17:51 -0700607 def getPullRequestEditedEvent(self):
608 return self._getPullRequestEvent('edited')
609
Gregory Haynes4fc12542015-04-22 20:38:06 -0700610 def addComment(self, message):
611 self.comments.append(message)
612 self._updateTimeStamp()
613
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200614 def getCommentAddedEvent(self, text):
615 name = 'issue_comment'
616 data = {
617 'action': 'created',
618 'issue': {
619 'number': self.number
620 },
621 'comment': {
622 'body': text
623 },
624 'repository': {
625 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100626 },
627 'sender': {
628 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200629 }
630 }
631 return (name, data)
632
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800633 def getReviewAddedEvent(self, review):
634 name = 'pull_request_review'
635 data = {
636 'action': 'submitted',
637 'pull_request': {
638 'number': self.number,
639 'title': self.subject,
640 'updated_at': self.updated_at,
641 'base': {
642 'ref': self.branch,
643 'repo': {
644 'full_name': self.project
645 }
646 },
647 'head': {
648 'sha': self.head_sha
649 }
650 },
651 'review': {
652 'state': review
653 },
654 'repository': {
655 'full_name': self.project
656 },
657 'sender': {
658 'login': 'ghuser'
659 }
660 }
661 return (name, data)
662
Jan Hruban16ad31f2015-11-07 14:39:07 +0100663 def addLabel(self, name):
664 if name not in self.labels:
665 self.labels.append(name)
666 self._updateTimeStamp()
667 return self._getLabelEvent(name)
668
669 def removeLabel(self, name):
670 if name in self.labels:
671 self.labels.remove(name)
672 self._updateTimeStamp()
673 return self._getUnlabelEvent(name)
674
675 def _getLabelEvent(self, label):
676 name = 'pull_request'
677 data = {
678 'action': 'labeled',
679 'pull_request': {
680 'number': self.number,
681 'updated_at': self.updated_at,
682 'base': {
683 'ref': self.branch,
684 'repo': {
685 'full_name': self.project
686 }
687 },
688 'head': {
689 'sha': self.head_sha
690 }
691 },
692 'label': {
693 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100694 },
695 'sender': {
696 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100697 }
698 }
699 return (name, data)
700
701 def _getUnlabelEvent(self, label):
702 name = 'pull_request'
703 data = {
704 'action': 'unlabeled',
705 'pull_request': {
706 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100707 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100708 'updated_at': self.updated_at,
709 'base': {
710 'ref': self.branch,
711 'repo': {
712 'full_name': self.project
713 }
714 },
715 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800716 'sha': self.head_sha,
717 'repo': {
718 'full_name': self.project
719 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100720 }
721 },
722 'label': {
723 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100724 },
725 'sender': {
726 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100727 }
728 }
729 return (name, data)
730
Jesse Keatinga41566f2017-06-14 18:17:51 -0700731 def editBody(self, body):
732 self.body = body
733 self._updateTimeStamp()
734
Gregory Haynes4fc12542015-04-22 20:38:06 -0700735 def _getRepo(self):
736 repo_path = os.path.join(self.upstream_root, self.project)
737 return git.Repo(repo_path)
738
739 def _createPRRef(self):
740 repo = self._getRepo()
741 GithubChangeReference.create(
742 repo, self._getPRReference(), 'refs/tags/init')
743
Jan Hruban570d01c2016-03-10 21:51:32 +0100744 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700745 repo = self._getRepo()
746 ref = repo.references[self._getPRReference()]
747 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100748 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700749 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100750 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700751 repo.head.reference = ref
752 zuul.merger.merger.reset_repo_to_head(repo)
753 repo.git.clean('-x', '-f', '-d')
754
Jan Hruban570d01c2016-03-10 21:51:32 +0100755 if files:
756 fn = files[0]
757 self.files = files
758 else:
759 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
760 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100761 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700762 fn = os.path.join(repo.working_dir, fn)
763 f = open(fn, 'w')
764 with open(fn, 'w') as f:
765 f.write("test %s %s\n" %
766 (self.branch, self.number))
767 repo.index.add([fn])
768
769 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800770 # Create an empty set of statuses for the given sha,
771 # each sha on a PR may have a status set on it
772 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700773 repo.head.reference = 'master'
774 zuul.merger.merger.reset_repo_to_head(repo)
775 repo.git.clean('-x', '-f', '-d')
776 repo.heads['master'].checkout()
777
778 def _updateTimeStamp(self):
779 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
780
781 def getPRHeadSha(self):
782 repo = self._getRepo()
783 return repo.references[self._getPRReference()].commit.hexsha
784
Jesse Keatingae4cd272017-01-30 17:10:44 -0800785 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800786 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
787 # convert the timestamp to a str format that would be returned
788 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800789
Adam Gandelmand81dd762017-02-09 15:15:49 -0800790 if granted_on:
791 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
792 submitted_at = time.strftime(
793 gh_time_format, granted_on.timetuple())
794 else:
795 # github timestamps only down to the second, so we need to make
796 # sure reviews that tests add appear to be added over a period of
797 # time in the past and not all at once.
798 if not self.reviews:
799 # the first review happens 10 mins ago
800 offset = 600
801 else:
802 # subsequent reviews happen 1 minute closer to now
803 offset = 600 - (len(self.reviews) * 60)
804
805 granted_on = datetime.datetime.utcfromtimestamp(
806 time.time() - offset)
807 submitted_at = time.strftime(
808 gh_time_format, granted_on.timetuple())
809
Jesse Keatingae4cd272017-01-30 17:10:44 -0800810 self.reviews.append({
811 'state': state,
812 'user': {
813 'login': user,
814 'email': user + "@derp.com",
815 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800816 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800817 })
818
Gregory Haynes4fc12542015-04-22 20:38:06 -0700819 def _getPRReference(self):
820 return '%s/head' % self.number
821
822 def _getPullRequestEvent(self, action):
823 name = 'pull_request'
824 data = {
825 'action': action,
826 'number': self.number,
827 'pull_request': {
828 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100829 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700830 'updated_at': self.updated_at,
831 'base': {
832 'ref': self.branch,
833 'repo': {
834 'full_name': self.project
835 }
836 },
837 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800838 'sha': self.head_sha,
839 'repo': {
840 'full_name': self.project
841 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700842 },
843 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100844 },
845 'sender': {
846 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700847 }
848 }
849 return (name, data)
850
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800851 def getCommitStatusEvent(self, context, state='success', user='zuul'):
852 name = 'status'
853 data = {
854 'state': state,
855 'sha': self.head_sha,
856 'description': 'Test results for %s: %s' % (self.head_sha, state),
857 'target_url': 'http://zuul/%s' % self.head_sha,
858 'branches': [],
859 'context': context,
860 'sender': {
861 'login': user
862 }
863 }
864 return (name, data)
865
Gregory Haynes4fc12542015-04-22 20:38:06 -0700866
867class FakeGithubConnection(githubconnection.GithubConnection):
868 log = logging.getLogger("zuul.test.FakeGithubConnection")
869
870 def __init__(self, driver, connection_name, connection_config,
871 upstream_root=None):
872 super(FakeGithubConnection, self).__init__(driver, connection_name,
873 connection_config)
874 self.connection_name = connection_name
875 self.pr_number = 0
876 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700877 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700878 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100879 self.merge_failure = False
880 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100881 self.reports = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700882
Jesse Keatinga41566f2017-06-14 18:17:51 -0700883 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700884 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700885 self.pr_number += 1
886 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100887 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700888 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700889 self.pull_requests.append(pull_request)
890 return pull_request
891
Jesse Keating71a47ff2017-06-06 11:36:43 -0700892 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
893 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700894 if not old_rev:
895 old_rev = '00000000000000000000000000000000'
896 if not new_rev:
897 new_rev = random_sha1()
898 name = 'push'
899 data = {
900 'ref': ref,
901 'before': old_rev,
902 'after': new_rev,
903 'repository': {
904 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700905 },
906 'commits': [
907 {
908 'added': added_files,
909 'removed': removed_files,
910 'modified': modified_files
911 }
912 ]
Wayne1a78c612015-06-11 17:14:13 -0700913 }
914 return (name, data)
915
Gregory Haynes4fc12542015-04-22 20:38:06 -0700916 def emitEvent(self, event):
917 """Emulates sending the GitHub webhook event to the connection."""
918 port = self.webapp.server.socket.getsockname()[1]
919 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700920 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -0700921 secret = self.connection_config['webhook_token']
922 signature = githubconnection._sign_request(payload, secret)
923 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700924 req = urllib.request.Request(
925 'http://localhost:%s/connection/%s/payload'
926 % (port, self.connection_name),
927 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000928 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700929
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200930 def getPull(self, project, number):
931 pr = self.pull_requests[number - 1]
932 data = {
933 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100934 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200935 'updated_at': pr.updated_at,
936 'base': {
937 'repo': {
938 'full_name': pr.project
939 },
940 'ref': pr.branch,
941 },
Jan Hruban37615e52015-11-19 14:30:49 +0100942 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700943 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200944 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800945 'sha': pr.head_sha,
946 'repo': {
947 'full_name': pr.project
948 }
Jesse Keating61040e72017-06-08 15:08:27 -0700949 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700950 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700951 'labels': pr.labels,
952 'merged': pr.is_merged,
953 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200954 }
955 return data
956
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800957 def getPullBySha(self, sha):
958 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
959 if len(prs) > 1:
960 raise Exception('Multiple pulls found with head sha: %s' % sha)
961 pr = prs[0]
962 return self.getPull(pr.project, pr.number)
963
Jesse Keatingae4cd272017-01-30 17:10:44 -0800964 def _getPullReviews(self, owner, project, number):
965 pr = self.pull_requests[number - 1]
966 return pr.reviews
967
Jan Hruban3b415922016-02-03 13:10:22 +0100968 def getUser(self, login):
969 data = {
970 'username': login,
971 'name': 'Github User',
972 'email': 'github.user@example.com'
973 }
974 return data
975
Jesse Keatingae4cd272017-01-30 17:10:44 -0800976 def getRepoPermission(self, project, login):
977 owner, proj = project.split('/')
978 for pr in self.pull_requests:
979 pr_owner, pr_project = pr.project.split('/')
980 if (pr_owner == owner and proj == pr_project):
981 if login in pr.writers:
982 return 'write'
983 else:
984 return 'read'
985
Gregory Haynes4fc12542015-04-22 20:38:06 -0700986 def getGitUrl(self, project):
987 return os.path.join(self.upstream_root, str(project))
988
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200989 def real_getGitUrl(self, project):
990 return super(FakeGithubConnection, self).getGitUrl(project)
991
Gregory Haynes4fc12542015-04-22 20:38:06 -0700992 def getProjectBranches(self, project):
993 """Masks getProjectBranches since we don't have a real github"""
994
995 # just returns master for now
996 return ['master']
997
Jan Hrubane252a732017-01-03 15:03:09 +0100998 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +0100999 # record that this got reported
1000 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001001 pull_request = self.pull_requests[pr_number - 1]
1002 pull_request.addComment(message)
1003
Jan Hruban3b415922016-02-03 13:10:22 +01001004 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001005 # record that this got reported
1006 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001007 pull_request = self.pull_requests[pr_number - 1]
1008 if self.merge_failure:
1009 raise Exception('Pull request was not merged')
1010 if self.merge_not_allowed_count > 0:
1011 self.merge_not_allowed_count -= 1
1012 raise MergeFailure('Merge was not successful due to mergeability'
1013 ' conflict')
1014 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001015 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001016
Jesse Keatingd96e5882017-01-19 13:55:50 -08001017 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001018 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001019
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001020 def setCommitStatus(self, project, sha, state, url='', description='',
1021 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001022 # record that this got reported
1023 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001024 # always insert a status to the front of the list, to represent
1025 # the last status provided for a commit.
1026 # Since we're bypassing github API, which would require a user, we
1027 # default the user as 'zuul' here.
1028 self.statuses.setdefault(project, {}).setdefault(sha, [])
1029 self.statuses[project][sha].insert(0, {
1030 'state': state,
1031 'url': url,
1032 'description': description,
1033 'context': context,
1034 'creator': {
1035 'login': user
1036 }
1037 })
Jan Hrubane252a732017-01-03 15:03:09 +01001038
Jan Hruban16ad31f2015-11-07 14:39:07 +01001039 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001040 # record that this got reported
1041 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001042 pull_request = self.pull_requests[pr_number - 1]
1043 pull_request.addLabel(label)
1044
1045 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001046 # record that this got reported
1047 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001048 pull_request = self.pull_requests[pr_number - 1]
1049 pull_request.removeLabel(label)
1050
Jesse Keatinga41566f2017-06-14 18:17:51 -07001051 def _getNeededByFromPR(self, change):
1052 prs = []
1053 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001054 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001055 change.number))
1056 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001057 if not pr.body:
1058 body = ''
1059 else:
1060 body = pr.body
1061 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001062 # Get our version of a pull so that it's a dict
1063 pull = self.getPull(pr.project, pr.number)
1064 prs.append(pull)
1065
1066 return prs
1067
Gregory Haynes4fc12542015-04-22 20:38:06 -07001068
Clark Boylanb640e052014-04-03 16:41:46 -07001069class BuildHistory(object):
1070 def __init__(self, **kw):
1071 self.__dict__.update(kw)
1072
1073 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001074 return ("<Completed build, result: %s name: %s uuid: %s "
1075 "changes: %s ref: %s>" %
1076 (self.result, self.name, self.uuid,
1077 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001078
1079
Clark Boylanb640e052014-04-03 16:41:46 -07001080class FakeStatsd(threading.Thread):
1081 def __init__(self):
1082 threading.Thread.__init__(self)
1083 self.daemon = True
1084 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1085 self.sock.bind(('', 0))
1086 self.port = self.sock.getsockname()[1]
1087 self.wake_read, self.wake_write = os.pipe()
1088 self.stats = []
1089
1090 def run(self):
1091 while True:
1092 poll = select.poll()
1093 poll.register(self.sock, select.POLLIN)
1094 poll.register(self.wake_read, select.POLLIN)
1095 ret = poll.poll()
1096 for (fd, event) in ret:
1097 if fd == self.sock.fileno():
1098 data = self.sock.recvfrom(1024)
1099 if not data:
1100 return
1101 self.stats.append(data[0])
1102 if fd == self.wake_read:
1103 return
1104
1105 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001106 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001107
1108
James E. Blaire1767bc2016-08-02 10:00:27 -07001109class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001110 log = logging.getLogger("zuul.test")
1111
Paul Belanger174a8272017-03-14 13:20:10 -04001112 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001113 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001114 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001115 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001116 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001117 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001118 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001119 # TODOv3(jeblair): self.node is really "the label of the node
1120 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001121 # keep using it like this, or we may end up exposing more of
1122 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001123 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001124 self.node = None
1125 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001126 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001127 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001128 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001129 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001130 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001131 self.wait_condition = threading.Condition()
1132 self.waiting = False
1133 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001134 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001135 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001136 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001137 items = self.parameters['zuul']['items']
1138 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1139 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001140
James E. Blair3158e282016-08-19 09:34:11 -07001141 def __repr__(self):
1142 waiting = ''
1143 if self.waiting:
1144 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001145 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1146 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001147
Clark Boylanb640e052014-04-03 16:41:46 -07001148 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001149 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001150 self.wait_condition.acquire()
1151 self.wait_condition.notify()
1152 self.waiting = False
1153 self.log.debug("Build %s released" % self.unique)
1154 self.wait_condition.release()
1155
1156 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001157 """Return whether this build is being held.
1158
1159 :returns: Whether the build is being held.
1160 :rtype: bool
1161 """
1162
Clark Boylanb640e052014-04-03 16:41:46 -07001163 self.wait_condition.acquire()
1164 if self.waiting:
1165 ret = True
1166 else:
1167 ret = False
1168 self.wait_condition.release()
1169 return ret
1170
1171 def _wait(self):
1172 self.wait_condition.acquire()
1173 self.waiting = True
1174 self.log.debug("Build %s waiting" % self.unique)
1175 self.wait_condition.wait()
1176 self.wait_condition.release()
1177
1178 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001179 self.log.debug('Running build %s' % self.unique)
1180
Paul Belanger174a8272017-03-14 13:20:10 -04001181 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001182 self.log.debug('Holding build %s' % self.unique)
1183 self._wait()
1184 self.log.debug("Build %s continuing" % self.unique)
1185
James E. Blair412fba82017-01-26 15:00:50 -08001186 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001187 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001188 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001189 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001190 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001191 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001192 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001193
James E. Blaire1767bc2016-08-02 10:00:27 -07001194 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001195
James E. Blaira5dba232016-08-08 15:53:24 -07001196 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001197 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001198 for change in changes:
1199 if self.hasChanges(change):
1200 return True
1201 return False
1202
James E. Blaire7b99a02016-08-05 14:27:34 -07001203 def hasChanges(self, *changes):
1204 """Return whether this build has certain changes in its git repos.
1205
1206 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001207 are expected to be present (in order) in the git repository of
1208 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001209
1210 :returns: Whether the build has the indicated changes.
1211 :rtype: bool
1212
1213 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001214 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001215 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001216 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001217 try:
1218 repo = git.Repo(path)
1219 except NoSuchPathError as e:
1220 self.log.debug('%s' % e)
1221 return False
James E. Blair247cab72017-07-20 16:52:36 -07001222 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001223 commit_message = '%s-1' % change.subject
1224 self.log.debug("Checking if build %s has changes; commit_message "
1225 "%s; repo_messages %s" % (self, commit_message,
1226 repo_messages))
1227 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001228 self.log.debug(" messages do not match")
1229 return False
1230 self.log.debug(" OK")
1231 return True
1232
James E. Blaird8af5422017-05-24 13:59:40 -07001233 def getWorkspaceRepos(self, projects):
1234 """Return workspace git repo objects for the listed projects
1235
1236 :arg list projects: A list of strings, each the canonical name
1237 of a project.
1238
1239 :returns: A dictionary of {name: repo} for every listed
1240 project.
1241 :rtype: dict
1242
1243 """
1244
1245 repos = {}
1246 for project in projects:
1247 path = os.path.join(self.jobdir.src_root, project)
1248 repo = git.Repo(path)
1249 repos[project] = repo
1250 return repos
1251
Clark Boylanb640e052014-04-03 16:41:46 -07001252
Paul Belanger174a8272017-03-14 13:20:10 -04001253class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1254 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001255
Paul Belanger174a8272017-03-14 13:20:10 -04001256 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001257 they will report that they have started but then pause until
1258 released before reporting completion. This attribute may be
1259 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001260 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001261 be explicitly released.
1262
1263 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001264 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001265 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001266 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001267 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001268 self.hold_jobs_in_build = False
1269 self.lock = threading.Lock()
1270 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001271 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001272 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001273 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001274
James E. Blaira5dba232016-08-08 15:53:24 -07001275 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001276 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001277
1278 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001279 :arg Change change: The :py:class:`~tests.base.FakeChange`
1280 instance which should cause the job to fail. This job
1281 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001282
1283 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001284 l = self.fail_tests.get(name, [])
1285 l.append(change)
1286 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001287
James E. Blair962220f2016-08-03 11:22:38 -07001288 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001289 """Release a held build.
1290
1291 :arg str regex: A regular expression which, if supplied, will
1292 cause only builds with matching names to be released. If
1293 not supplied, all builds will be released.
1294
1295 """
James E. Blair962220f2016-08-03 11:22:38 -07001296 builds = self.running_builds[:]
1297 self.log.debug("Releasing build %s (%s)" % (regex,
1298 len(self.running_builds)))
1299 for build in builds:
1300 if not regex or re.match(regex, build.name):
1301 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001302 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001303 build.release()
1304 else:
1305 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001306 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001307 self.log.debug("Done releasing builds %s (%s)" %
1308 (regex, len(self.running_builds)))
1309
Paul Belanger174a8272017-03-14 13:20:10 -04001310 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001311 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001312 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001313 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001314 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001315 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001316 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001317 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001318 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1319 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001320
1321 def stopJob(self, job):
1322 self.log.debug("handle stop")
1323 parameters = json.loads(job.arguments)
1324 uuid = parameters['uuid']
1325 for build in self.running_builds:
1326 if build.unique == uuid:
1327 build.aborted = True
1328 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001329 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001330
James E. Blaira002b032017-04-18 10:35:48 -07001331 def stop(self):
1332 for build in self.running_builds:
1333 build.release()
1334 super(RecordingExecutorServer, self).stop()
1335
Joshua Hesketh50c21782016-10-13 21:34:14 +11001336
Paul Belanger174a8272017-03-14 13:20:10 -04001337class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001338 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001339 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001340 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001341 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001342 if not commit: # merge conflict
1343 self.recordResult('MERGER_FAILURE')
1344 return commit
1345
1346 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001347 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001348 self.executor_server.lock.acquire()
1349 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001350 BuildHistory(name=build.name, result=result, changes=build.changes,
1351 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001352 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001353 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001354 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001355 )
Paul Belanger174a8272017-03-14 13:20:10 -04001356 self.executor_server.running_builds.remove(build)
1357 del self.executor_server.job_builds[self.job.unique]
1358 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001359
1360 def runPlaybooks(self, args):
1361 build = self.executor_server.job_builds[self.job.unique]
1362 build.jobdir = self.jobdir
1363
1364 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1365 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001366 return result
1367
James E. Blair74a82cf2017-07-12 17:23:08 -07001368 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001369 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001370
Paul Belanger174a8272017-03-14 13:20:10 -04001371 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001372 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001373 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001374 else:
1375 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001376 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001377
James E. Blairad8dca02017-02-21 11:48:32 -05001378 def getHostList(self, args):
1379 self.log.debug("hostlist")
1380 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001381 for host in hosts:
1382 host['host_vars']['ansible_connection'] = 'local'
1383
1384 hosts.append(dict(
1385 name='localhost',
1386 host_vars=dict(ansible_connection='local'),
1387 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001388 return hosts
1389
James E. Blairf5dbd002015-12-23 15:26:17 -08001390
Clark Boylanb640e052014-04-03 16:41:46 -07001391class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001392 """A Gearman server for use in tests.
1393
1394 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1395 added to the queue but will not be distributed to workers
1396 until released. This attribute may be changed at any time and
1397 will take effect for subsequently enqueued jobs, but
1398 previously held jobs will still need to be explicitly
1399 released.
1400
1401 """
1402
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001403 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001404 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001405 if use_ssl:
1406 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1407 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1408 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1409 else:
1410 ssl_ca = None
1411 ssl_cert = None
1412 ssl_key = None
1413
1414 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1415 ssl_cert=ssl_cert,
1416 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001417
1418 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001419 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1420 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001421 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001422 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001423 job.waiting = self.hold_jobs_in_queue
1424 else:
1425 job.waiting = False
1426 if job.waiting:
1427 continue
1428 if job.name in connection.functions:
1429 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001430 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001431 connection.related_jobs[job.handle] = job
1432 job.worker_connection = connection
1433 job.running = True
1434 return job
1435 return None
1436
1437 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001438 """Release a held job.
1439
1440 :arg str regex: A regular expression which, if supplied, will
1441 cause only jobs with matching names to be released. If
1442 not supplied, all jobs will be released.
1443 """
Clark Boylanb640e052014-04-03 16:41:46 -07001444 released = False
1445 qlen = (len(self.high_queue) + len(self.normal_queue) +
1446 len(self.low_queue))
1447 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1448 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001449 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001450 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001451 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001452 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001453 self.log.debug("releasing queued job %s" %
1454 job.unique)
1455 job.waiting = False
1456 released = True
1457 else:
1458 self.log.debug("not releasing queued job %s" %
1459 job.unique)
1460 if released:
1461 self.wakeConnections()
1462 qlen = (len(self.high_queue) + len(self.normal_queue) +
1463 len(self.low_queue))
1464 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1465
1466
1467class FakeSMTP(object):
1468 log = logging.getLogger('zuul.FakeSMTP')
1469
1470 def __init__(self, messages, server, port):
1471 self.server = server
1472 self.port = port
1473 self.messages = messages
1474
1475 def sendmail(self, from_email, to_email, msg):
1476 self.log.info("Sending email from %s, to %s, with msg %s" % (
1477 from_email, to_email, msg))
1478
1479 headers = msg.split('\n\n', 1)[0]
1480 body = msg.split('\n\n', 1)[1]
1481
1482 self.messages.append(dict(
1483 from_email=from_email,
1484 to_email=to_email,
1485 msg=msg,
1486 headers=headers,
1487 body=body,
1488 ))
1489
1490 return True
1491
1492 def quit(self):
1493 return True
1494
1495
James E. Blairdce6cea2016-12-20 16:45:32 -08001496class FakeNodepool(object):
1497 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001498 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001499
1500 log = logging.getLogger("zuul.test.FakeNodepool")
1501
1502 def __init__(self, host, port, chroot):
1503 self.client = kazoo.client.KazooClient(
1504 hosts='%s:%s%s' % (host, port, chroot))
1505 self.client.start()
1506 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001507 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001508 self.thread = threading.Thread(target=self.run)
1509 self.thread.daemon = True
1510 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001511 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001512
1513 def stop(self):
1514 self._running = False
1515 self.thread.join()
1516 self.client.stop()
1517 self.client.close()
1518
1519 def run(self):
1520 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001521 try:
1522 self._run()
1523 except Exception:
1524 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001525 time.sleep(0.1)
1526
1527 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001528 if self.paused:
1529 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001530 for req in self.getNodeRequests():
1531 self.fulfillRequest(req)
1532
1533 def getNodeRequests(self):
1534 try:
1535 reqids = self.client.get_children(self.REQUEST_ROOT)
1536 except kazoo.exceptions.NoNodeError:
1537 return []
1538 reqs = []
1539 for oid in sorted(reqids):
1540 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001541 try:
1542 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001543 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001544 data['_oid'] = oid
1545 reqs.append(data)
1546 except kazoo.exceptions.NoNodeError:
1547 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001548 return reqs
1549
James E. Blaire18d4602017-01-05 11:17:28 -08001550 def getNodes(self):
1551 try:
1552 nodeids = self.client.get_children(self.NODE_ROOT)
1553 except kazoo.exceptions.NoNodeError:
1554 return []
1555 nodes = []
1556 for oid in sorted(nodeids):
1557 path = self.NODE_ROOT + '/' + oid
1558 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001559 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001560 data['_oid'] = oid
1561 try:
1562 lockfiles = self.client.get_children(path + '/lock')
1563 except kazoo.exceptions.NoNodeError:
1564 lockfiles = []
1565 if lockfiles:
1566 data['_lock'] = True
1567 else:
1568 data['_lock'] = False
1569 nodes.append(data)
1570 return nodes
1571
James E. Blaira38c28e2017-01-04 10:33:20 -08001572 def makeNode(self, request_id, node_type):
1573 now = time.time()
1574 path = '/nodepool/nodes/'
1575 data = dict(type=node_type,
1576 provider='test-provider',
1577 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001578 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001579 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001580 public_ipv4='127.0.0.1',
1581 private_ipv4=None,
1582 public_ipv6=None,
1583 allocated_to=request_id,
1584 state='ready',
1585 state_time=now,
1586 created_time=now,
1587 updated_time=now,
1588 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001589 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001590 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001591 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001592 path = self.client.create(path, data,
1593 makepath=True,
1594 sequence=True)
1595 nodeid = path.split("/")[-1]
1596 return nodeid
1597
James E. Blair6ab79e02017-01-06 10:10:17 -08001598 def addFailRequest(self, request):
1599 self.fail_requests.add(request['_oid'])
1600
James E. Blairdce6cea2016-12-20 16:45:32 -08001601 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001602 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001603 return
1604 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001605 oid = request['_oid']
1606 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001607
James E. Blair6ab79e02017-01-06 10:10:17 -08001608 if oid in self.fail_requests:
1609 request['state'] = 'failed'
1610 else:
1611 request['state'] = 'fulfilled'
1612 nodes = []
1613 for node in request['node_types']:
1614 nodeid = self.makeNode(oid, node)
1615 nodes.append(nodeid)
1616 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001617
James E. Blaira38c28e2017-01-04 10:33:20 -08001618 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001619 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001620 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001621 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001622 try:
1623 self.client.set(path, data)
1624 except kazoo.exceptions.NoNodeError:
1625 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001626
1627
James E. Blair498059b2016-12-20 13:50:13 -08001628class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001629 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001630 super(ChrootedKazooFixture, self).__init__()
1631
1632 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1633 if ':' in zk_host:
1634 host, port = zk_host.split(':')
1635 else:
1636 host = zk_host
1637 port = None
1638
1639 self.zookeeper_host = host
1640
1641 if not port:
1642 self.zookeeper_port = 2181
1643 else:
1644 self.zookeeper_port = int(port)
1645
Clark Boylan621ec9a2017-04-07 17:41:33 -07001646 self.test_id = test_id
1647
James E. Blair498059b2016-12-20 13:50:13 -08001648 def _setUp(self):
1649 # Make sure the test chroot paths do not conflict
1650 random_bits = ''.join(random.choice(string.ascii_lowercase +
1651 string.ascii_uppercase)
1652 for x in range(8))
1653
Clark Boylan621ec9a2017-04-07 17:41:33 -07001654 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001655 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1656
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001657 self.addCleanup(self._cleanup)
1658
James E. Blair498059b2016-12-20 13:50:13 -08001659 # Ensure the chroot path exists and clean up any pre-existing znodes.
1660 _tmp_client = kazoo.client.KazooClient(
1661 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1662 _tmp_client.start()
1663
1664 if _tmp_client.exists(self.zookeeper_chroot):
1665 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1666
1667 _tmp_client.ensure_path(self.zookeeper_chroot)
1668 _tmp_client.stop()
1669 _tmp_client.close()
1670
James E. Blair498059b2016-12-20 13:50:13 -08001671 def _cleanup(self):
1672 '''Remove the chroot path.'''
1673 # Need a non-chroot'ed client to remove the chroot path
1674 _tmp_client = kazoo.client.KazooClient(
1675 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1676 _tmp_client.start()
1677 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1678 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001679 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001680
1681
Joshua Heskethd78b4482015-09-14 16:56:34 -06001682class MySQLSchemaFixture(fixtures.Fixture):
1683 def setUp(self):
1684 super(MySQLSchemaFixture, self).setUp()
1685
1686 random_bits = ''.join(random.choice(string.ascii_lowercase +
1687 string.ascii_uppercase)
1688 for x in range(8))
1689 self.name = '%s_%s' % (random_bits, os.getpid())
1690 self.passwd = uuid.uuid4().hex
1691 db = pymysql.connect(host="localhost",
1692 user="openstack_citest",
1693 passwd="openstack_citest",
1694 db="openstack_citest")
1695 cur = db.cursor()
1696 cur.execute("create database %s" % self.name)
1697 cur.execute(
1698 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1699 (self.name, self.name, self.passwd))
1700 cur.execute("flush privileges")
1701
1702 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1703 self.passwd,
1704 self.name)
1705 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1706 self.addCleanup(self.cleanup)
1707
1708 def cleanup(self):
1709 db = pymysql.connect(host="localhost",
1710 user="openstack_citest",
1711 passwd="openstack_citest",
1712 db="openstack_citest")
1713 cur = db.cursor()
1714 cur.execute("drop database %s" % self.name)
1715 cur.execute("drop user '%s'@'localhost'" % self.name)
1716 cur.execute("flush privileges")
1717
1718
Maru Newby3fe5f852015-01-13 04:22:14 +00001719class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001720 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001721 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001722
James E. Blair1c236df2017-02-01 14:07:24 -08001723 def attachLogs(self, *args):
1724 def reader():
1725 self._log_stream.seek(0)
1726 while True:
1727 x = self._log_stream.read(4096)
1728 if not x:
1729 break
1730 yield x.encode('utf8')
1731 content = testtools.content.content_from_reader(
1732 reader,
1733 testtools.content_type.UTF8_TEXT,
1734 False)
1735 self.addDetail('logging', content)
1736
Clark Boylanb640e052014-04-03 16:41:46 -07001737 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001738 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001739 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1740 try:
1741 test_timeout = int(test_timeout)
1742 except ValueError:
1743 # If timeout value is invalid do not set a timeout.
1744 test_timeout = 0
1745 if test_timeout > 0:
1746 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1747
1748 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1749 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1750 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1751 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1752 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1753 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1754 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1755 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1756 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1757 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001758 self._log_stream = StringIO()
1759 self.addOnException(self.attachLogs)
1760 else:
1761 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001762
James E. Blair73b41772017-05-22 13:22:55 -07001763 # NOTE(jeblair): this is temporary extra debugging to try to
1764 # track down a possible leak.
1765 orig_git_repo_init = git.Repo.__init__
1766
1767 def git_repo_init(myself, *args, **kw):
1768 orig_git_repo_init(myself, *args, **kw)
1769 self.log.debug("Created git repo 0x%x %s" %
1770 (id(myself), repr(myself)))
1771
1772 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1773 git_repo_init))
1774
James E. Blair1c236df2017-02-01 14:07:24 -08001775 handler = logging.StreamHandler(self._log_stream)
1776 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1777 '%(levelname)-8s %(message)s')
1778 handler.setFormatter(formatter)
1779
1780 logger = logging.getLogger()
1781 logger.setLevel(logging.DEBUG)
1782 logger.addHandler(handler)
1783
Clark Boylan3410d532017-04-25 12:35:29 -07001784 # Make sure we don't carry old handlers around in process state
1785 # which slows down test runs
1786 self.addCleanup(logger.removeHandler, handler)
1787 self.addCleanup(handler.close)
1788 self.addCleanup(handler.flush)
1789
James E. Blair1c236df2017-02-01 14:07:24 -08001790 # NOTE(notmorgan): Extract logging overrides for specific
1791 # libraries from the OS_LOG_DEFAULTS env and create loggers
1792 # for each. This is used to limit the output during test runs
1793 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001794 log_defaults_from_env = os.environ.get(
1795 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001796 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001797
James E. Blairdce6cea2016-12-20 16:45:32 -08001798 if log_defaults_from_env:
1799 for default in log_defaults_from_env.split(','):
1800 try:
1801 name, level_str = default.split('=', 1)
1802 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001803 logger = logging.getLogger(name)
1804 logger.setLevel(level)
1805 logger.addHandler(handler)
1806 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 except ValueError:
1808 # NOTE(notmorgan): Invalid format of the log default,
1809 # skip and don't try and apply a logger for the
1810 # specified module
1811 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001812
Maru Newby3fe5f852015-01-13 04:22:14 +00001813
1814class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001815 """A test case with a functioning Zuul.
1816
1817 The following class variables are used during test setup and can
1818 be overidden by subclasses but are effectively read-only once a
1819 test method starts running:
1820
1821 :cvar str config_file: This points to the main zuul config file
1822 within the fixtures directory. Subclasses may override this
1823 to obtain a different behavior.
1824
1825 :cvar str tenant_config_file: This is the tenant config file
1826 (which specifies from what git repos the configuration should
1827 be loaded). It defaults to the value specified in
1828 `config_file` but can be overidden by subclasses to obtain a
1829 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001830 configuration. See also the :py:func:`simple_layout`
1831 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001832
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001833 :cvar bool create_project_keys: Indicates whether Zuul should
1834 auto-generate keys for each project, or whether the test
1835 infrastructure should insert dummy keys to save time during
1836 startup. Defaults to False.
1837
James E. Blaire7b99a02016-08-05 14:27:34 -07001838 The following are instance variables that are useful within test
1839 methods:
1840
1841 :ivar FakeGerritConnection fake_<connection>:
1842 A :py:class:`~tests.base.FakeGerritConnection` will be
1843 instantiated for each connection present in the config file
1844 and stored here. For instance, `fake_gerrit` will hold the
1845 FakeGerritConnection object for a connection named `gerrit`.
1846
1847 :ivar FakeGearmanServer gearman_server: An instance of
1848 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1849 server that all of the Zuul components in this test use to
1850 communicate with each other.
1851
Paul Belanger174a8272017-03-14 13:20:10 -04001852 :ivar RecordingExecutorServer executor_server: An instance of
1853 :py:class:`~tests.base.RecordingExecutorServer` which is the
1854 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001855
1856 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1857 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001858 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001859 list upon completion.
1860
1861 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1862 objects representing completed builds. They are appended to
1863 the list in the order they complete.
1864
1865 """
1866
James E. Blair83005782015-12-11 14:46:03 -08001867 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001868 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001869 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001870 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001871
1872 def _startMerger(self):
1873 self.merge_server = zuul.merger.server.MergeServer(self.config,
1874 self.connections)
1875 self.merge_server.start()
1876
Maru Newby3fe5f852015-01-13 04:22:14 +00001877 def setUp(self):
1878 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001879
1880 self.setupZK()
1881
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001882 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001883 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001884 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1885 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001886 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001887 tmp_root = tempfile.mkdtemp(
1888 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001889 self.test_root = os.path.join(tmp_root, "zuul-test")
1890 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001891 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001892 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001893 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001894 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1895 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001896
1897 if os.path.exists(self.test_root):
1898 shutil.rmtree(self.test_root)
1899 os.makedirs(self.test_root)
1900 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001901 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001902 os.makedirs(self.merger_state_root)
1903 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001904
1905 # Make per test copy of Configuration.
1906 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001907 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1908 if not os.path.exists(self.private_key_file):
1909 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1910 shutil.copy(src_private_key_file, self.private_key_file)
1911 shutil.copy('{}.pub'.format(src_private_key_file),
1912 '{}.pub'.format(self.private_key_file))
1913 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001914 self.config.set('scheduler', 'tenant_config',
1915 os.path.join(
1916 FIXTURE_DIR,
1917 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001918 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001919 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001920 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001921 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001922 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001923 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001924
Clark Boylanb640e052014-04-03 16:41:46 -07001925 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001926 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1927 # see: https://github.com/jsocol/pystatsd/issues/61
1928 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001929 os.environ['STATSD_PORT'] = str(self.statsd.port)
1930 self.statsd.start()
1931 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001932 importlib.reload(statsd)
1933 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001934
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001935 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001936
1937 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001938 self.log.info("Gearman server on port %s" %
1939 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001940 if self.use_ssl:
1941 self.log.info('SSL enabled for gearman')
1942 self.config.set(
1943 'gearman', 'ssl_ca',
1944 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1945 self.config.set(
1946 'gearman', 'ssl_cert',
1947 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1948 self.config.set(
1949 'gearman', 'ssl_key',
1950 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001951
James E. Blaire511d2f2016-12-08 15:22:26 -08001952 gerritsource.GerritSource.replication_timeout = 1.5
1953 gerritsource.GerritSource.replication_retry_interval = 0.5
1954 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001955
Joshua Hesketh352264b2015-08-11 23:42:08 +10001956 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001957
Jan Hruban7083edd2015-08-21 14:00:54 +02001958 self.webapp = zuul.webapp.WebApp(
1959 self.sched, port=0, listen_address='127.0.0.1')
1960
Jan Hruban6b71aff2015-10-22 16:58:08 +02001961 self.event_queues = [
1962 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001963 self.sched.trigger_event_queue,
1964 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001965 ]
1966
James E. Blairfef78942016-03-11 16:28:56 -08001967 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001968 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001969
Paul Belanger174a8272017-03-14 13:20:10 -04001970 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001971 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001972 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001973 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001974 _test_root=self.test_root,
1975 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001976 self.executor_server.start()
1977 self.history = self.executor_server.build_history
1978 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001979
Paul Belanger174a8272017-03-14 13:20:10 -04001980 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001981 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001982 self.merge_client = zuul.merger.client.MergeClient(
1983 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001984 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001985 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001986 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001987
James E. Blair0d5a36e2017-02-21 10:53:44 -05001988 self.fake_nodepool = FakeNodepool(
1989 self.zk_chroot_fixture.zookeeper_host,
1990 self.zk_chroot_fixture.zookeeper_port,
1991 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001992
Paul Belanger174a8272017-03-14 13:20:10 -04001993 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001994 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001995 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001996 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001997
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001998 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001999
2000 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002001 self.webapp.start()
2002 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002003 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002004 # Cleanups are run in reverse order
2005 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002006 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002007 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002008
James E. Blairb9c0d772017-03-03 14:34:49 -08002009 self.sched.reconfigure(self.config)
2010 self.sched.resume()
2011
Tobias Henkel7df274b2017-05-26 17:41:11 +02002012 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002013 # Set up gerrit related fakes
2014 # Set a changes database so multiple FakeGerrit's can report back to
2015 # a virtual canonical database given by the configured hostname
2016 self.gerrit_changes_dbs = {}
2017
2018 def getGerritConnection(driver, name, config):
2019 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2020 con = FakeGerritConnection(driver, name, config,
2021 changes_db=db,
2022 upstream_root=self.upstream_root)
2023 self.event_queues.append(con.event_queue)
2024 setattr(self, 'fake_' + name, con)
2025 return con
2026
2027 self.useFixture(fixtures.MonkeyPatch(
2028 'zuul.driver.gerrit.GerritDriver.getConnection',
2029 getGerritConnection))
2030
Gregory Haynes4fc12542015-04-22 20:38:06 -07002031 def getGithubConnection(driver, name, config):
2032 con = FakeGithubConnection(driver, name, config,
2033 upstream_root=self.upstream_root)
2034 setattr(self, 'fake_' + name, con)
2035 return con
2036
2037 self.useFixture(fixtures.MonkeyPatch(
2038 'zuul.driver.github.GithubDriver.getConnection',
2039 getGithubConnection))
2040
James E. Blaire511d2f2016-12-08 15:22:26 -08002041 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002042 # TODO(jhesketh): This should come from lib.connections for better
2043 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002044 # Register connections from the config
2045 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002046
Joshua Hesketh352264b2015-08-11 23:42:08 +10002047 def FakeSMTPFactory(*args, **kw):
2048 args = [self.smtp_messages] + list(args)
2049 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002050
Joshua Hesketh352264b2015-08-11 23:42:08 +10002051 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002052
James E. Blaire511d2f2016-12-08 15:22:26 -08002053 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002054 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002055 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002056
James E. Blair83005782015-12-11 14:46:03 -08002057 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002058 # This creates the per-test configuration object. It can be
2059 # overriden by subclasses, but should not need to be since it
2060 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002061 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002062 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002063
James E. Blair39840362017-06-23 20:34:02 +01002064 sections = ['zuul', 'scheduler', 'executor', 'merger']
2065 for section in sections:
2066 if not self.config.has_section(section):
2067 self.config.add_section(section)
2068
James E. Blair06cc3922017-04-19 10:08:10 -07002069 if not self.setupSimpleLayout():
2070 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002071 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002072 self.tenant_config_file)
2073 git_path = os.path.join(
2074 os.path.dirname(
2075 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2076 'git')
2077 if os.path.exists(git_path):
2078 for reponame in os.listdir(git_path):
2079 project = reponame.replace('_', '/')
2080 self.copyDirToRepo(project,
2081 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002082 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002083 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002084 self.setupAllProjectKeys()
2085
James E. Blair06cc3922017-04-19 10:08:10 -07002086 def setupSimpleLayout(self):
2087 # If the test method has been decorated with a simple_layout,
2088 # use that instead of the class tenant_config_file. Set up a
2089 # single config-project with the specified layout, and
2090 # initialize repos for all of the 'project' entries which
2091 # appear in the layout.
2092 test_name = self.id().split('.')[-1]
2093 test = getattr(self, test_name)
2094 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002095 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002096 else:
2097 return False
2098
James E. Blairb70e55a2017-04-19 12:57:02 -07002099 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002100 path = os.path.join(FIXTURE_DIR, path)
2101 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002102 data = f.read()
2103 layout = yaml.safe_load(data)
2104 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002105 untrusted_projects = []
2106 for item in layout:
2107 if 'project' in item:
2108 name = item['project']['name']
2109 untrusted_projects.append(name)
2110 self.init_repo(name)
2111 self.addCommitToRepo(name, 'initial commit',
2112 files={'README': ''},
2113 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002114 if 'job' in item:
2115 jobname = item['job']['name']
2116 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002117
2118 root = os.path.join(self.test_root, "config")
2119 if not os.path.exists(root):
2120 os.makedirs(root)
2121 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2122 config = [{'tenant':
2123 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002124 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002125 {'config-projects': ['common-config'],
2126 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002127 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002128 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002129 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002130 os.path.join(FIXTURE_DIR, f.name))
2131
2132 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002133 self.addCommitToRepo('common-config', 'add content from fixture',
2134 files, branch='master', tag='init')
2135
2136 return True
2137
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002138 def setupAllProjectKeys(self):
2139 if self.create_project_keys:
2140 return
2141
James E. Blair39840362017-06-23 20:34:02 +01002142 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002143 with open(os.path.join(FIXTURE_DIR, path)) as f:
2144 tenant_config = yaml.safe_load(f.read())
2145 for tenant in tenant_config:
2146 sources = tenant['tenant']['source']
2147 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002148 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002149 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002150 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002151 self.setupProjectKeys(source, project)
2152
2153 def setupProjectKeys(self, source, project):
2154 # Make sure we set up an RSA key for the project so that we
2155 # don't spend time generating one:
2156
James E. Blair6459db12017-06-29 14:57:20 -07002157 if isinstance(project, dict):
2158 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002159 key_root = os.path.join(self.state_root, 'keys')
2160 if not os.path.isdir(key_root):
2161 os.mkdir(key_root, 0o700)
2162 private_key_file = os.path.join(key_root, source, project + '.pem')
2163 private_key_dir = os.path.dirname(private_key_file)
2164 self.log.debug("Installing test keys for project %s at %s" % (
2165 project, private_key_file))
2166 if not os.path.isdir(private_key_dir):
2167 os.makedirs(private_key_dir)
2168 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2169 with open(private_key_file, 'w') as o:
2170 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002171
James E. Blair498059b2016-12-20 13:50:13 -08002172 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002173 self.zk_chroot_fixture = self.useFixture(
2174 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002175 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002176 self.zk_chroot_fixture.zookeeper_host,
2177 self.zk_chroot_fixture.zookeeper_port,
2178 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002179
James E. Blair96c6bf82016-01-15 16:20:40 -08002180 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002181 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002182
2183 files = {}
2184 for (dirpath, dirnames, filenames) in os.walk(source_path):
2185 for filename in filenames:
2186 test_tree_filepath = os.path.join(dirpath, filename)
2187 common_path = os.path.commonprefix([test_tree_filepath,
2188 source_path])
2189 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2190 with open(test_tree_filepath, 'r') as f:
2191 content = f.read()
2192 files[relative_filepath] = content
2193 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002194 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002195
James E. Blaire18d4602017-01-05 11:17:28 -08002196 def assertNodepoolState(self):
2197 # Make sure that there are no pending requests
2198
2199 requests = self.fake_nodepool.getNodeRequests()
2200 self.assertEqual(len(requests), 0)
2201
2202 nodes = self.fake_nodepool.getNodes()
2203 for node in nodes:
2204 self.assertFalse(node['_lock'], "Node %s is locked" %
2205 (node['_oid'],))
2206
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002207 def assertNoGeneratedKeys(self):
2208 # Make sure that Zuul did not generate any project keys
2209 # (unless it was supposed to).
2210
2211 if self.create_project_keys:
2212 return
2213
2214 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2215 test_key = i.read()
2216
2217 key_root = os.path.join(self.state_root, 'keys')
2218 for root, dirname, files in os.walk(key_root):
2219 for fn in files:
2220 with open(os.path.join(root, fn)) as f:
2221 self.assertEqual(test_key, f.read())
2222
Clark Boylanb640e052014-04-03 16:41:46 -07002223 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002224 self.log.debug("Assert final state")
2225 # Make sure no jobs are running
2226 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002227 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002228 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002229 gc.collect()
2230 for obj in gc.get_objects():
2231 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002232 self.log.debug("Leaked git repo object: 0x%x %s" %
2233 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002234 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002235 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002236 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002237 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002238 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002239 for tenant in self.sched.abide.tenants.values():
2240 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002241 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002242 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002243
2244 def shutdown(self):
2245 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002246 self.executor_server.hold_jobs_in_build = False
2247 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002248 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002249 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002250 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002251 self.sched.stop()
2252 self.sched.join()
2253 self.statsd.stop()
2254 self.statsd.join()
2255 self.webapp.stop()
2256 self.webapp.join()
2257 self.rpc.stop()
2258 self.rpc.join()
2259 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002260 self.fake_nodepool.stop()
2261 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002262 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002263 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002264 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002265 # Further the pydevd threads also need to be whitelisted so debugging
2266 # e.g. in PyCharm is possible without breaking shutdown.
2267 whitelist = ['executor-watchdog',
2268 'pydevd.CommandThread',
2269 'pydevd.Reader',
2270 'pydevd.Writer',
2271 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002272 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002273 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002274 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002275 log_str = ""
2276 for thread_id, stack_frame in sys._current_frames().items():
2277 log_str += "Thread: %s\n" % thread_id
2278 log_str += "".join(traceback.format_stack(stack_frame))
2279 self.log.debug(log_str)
2280 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002281
James E. Blaira002b032017-04-18 10:35:48 -07002282 def assertCleanShutdown(self):
2283 pass
2284
James E. Blairc4ba97a2017-04-19 16:26:24 -07002285 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002286 parts = project.split('/')
2287 path = os.path.join(self.upstream_root, *parts[:-1])
2288 if not os.path.exists(path):
2289 os.makedirs(path)
2290 path = os.path.join(self.upstream_root, project)
2291 repo = git.Repo.init(path)
2292
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002293 with repo.config_writer() as config_writer:
2294 config_writer.set_value('user', 'email', 'user@example.com')
2295 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002296
Clark Boylanb640e052014-04-03 16:41:46 -07002297 repo.index.commit('initial commit')
2298 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002299 if tag:
2300 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002301
James E. Blair97d902e2014-08-21 13:25:56 -07002302 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002303 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002304 repo.git.clean('-x', '-f', '-d')
2305
James E. Blair97d902e2014-08-21 13:25:56 -07002306 def create_branch(self, project, branch):
2307 path = os.path.join(self.upstream_root, project)
2308 repo = git.Repo.init(path)
2309 fn = os.path.join(path, 'README')
2310
2311 branch_head = repo.create_head(branch)
2312 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002313 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002314 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002315 f.close()
2316 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002317 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002318
James E. Blair97d902e2014-08-21 13:25:56 -07002319 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002320 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002321 repo.git.clean('-x', '-f', '-d')
2322
Sachi King9f16d522016-03-16 12:20:45 +11002323 def create_commit(self, project):
2324 path = os.path.join(self.upstream_root, project)
2325 repo = git.Repo(path)
2326 repo.head.reference = repo.heads['master']
2327 file_name = os.path.join(path, 'README')
2328 with open(file_name, 'a') as f:
2329 f.write('creating fake commit\n')
2330 repo.index.add([file_name])
2331 commit = repo.index.commit('Creating a fake commit')
2332 return commit.hexsha
2333
James E. Blairf4a5f022017-04-18 14:01:10 -07002334 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002335 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002336 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002337 while len(self.builds):
2338 self.release(self.builds[0])
2339 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002340 i += 1
2341 if count is not None and i >= count:
2342 break
James E. Blairb8c16472015-05-05 14:55:26 -07002343
James E. Blairdf25ddc2017-07-08 07:57:09 -07002344 def getSortedBuilds(self):
2345 "Return the list of currently running builds sorted by name"
2346
2347 return sorted(self.builds, key=lambda x: x.name)
2348
Clark Boylanb640e052014-04-03 16:41:46 -07002349 def release(self, job):
2350 if isinstance(job, FakeBuild):
2351 job.release()
2352 else:
2353 job.waiting = False
2354 self.log.debug("Queued job %s released" % job.unique)
2355 self.gearman_server.wakeConnections()
2356
2357 def getParameter(self, job, name):
2358 if isinstance(job, FakeBuild):
2359 return job.parameters[name]
2360 else:
2361 parameters = json.loads(job.arguments)
2362 return parameters[name]
2363
Clark Boylanb640e052014-04-03 16:41:46 -07002364 def haveAllBuildsReported(self):
2365 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002366 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002367 return False
2368 # Find out if every build that the worker has completed has been
2369 # reported back to Zuul. If it hasn't then that means a Gearman
2370 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002371 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002372 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002373 if not zbuild:
2374 # It has already been reported
2375 continue
2376 # It hasn't been reported yet.
2377 return False
2378 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002379 worker = self.executor_server.executor_worker
2380 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002381 if connection.state == 'GRAB_WAIT':
2382 return False
2383 return True
2384
2385 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002386 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002387 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002388 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002389 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002390 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002391 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002392 for j in conn.related_jobs.values():
2393 if j.unique == build.uuid:
2394 client_job = j
2395 break
2396 if not client_job:
2397 self.log.debug("%s is not known to the gearman client" %
2398 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002399 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002400 if not client_job.handle:
2401 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002402 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002403 server_job = self.gearman_server.jobs.get(client_job.handle)
2404 if not server_job:
2405 self.log.debug("%s is not known to the gearman server" %
2406 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002407 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002408 if not hasattr(server_job, 'waiting'):
2409 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002410 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002411 if server_job.waiting:
2412 continue
James E. Blair17302972016-08-10 16:11:42 -07002413 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002414 self.log.debug("%s has not reported start" % build)
2415 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002416 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002417 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002418 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002419 if worker_build:
2420 if worker_build.isWaiting():
2421 continue
2422 else:
2423 self.log.debug("%s is running" % worker_build)
2424 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002425 else:
James E. Blair962220f2016-08-03 11:22:38 -07002426 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002427 return False
James E. Blaira002b032017-04-18 10:35:48 -07002428 for (build_uuid, job_worker) in \
2429 self.executor_server.job_workers.items():
2430 if build_uuid not in seen_builds:
2431 self.log.debug("%s is not finalized" % build_uuid)
2432 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002433 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002434
James E. Blairdce6cea2016-12-20 16:45:32 -08002435 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002436 if self.fake_nodepool.paused:
2437 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002438 if self.sched.nodepool.requests:
2439 return False
2440 return True
2441
Jan Hruban6b71aff2015-10-22 16:58:08 +02002442 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002443 for event_queue in self.event_queues:
2444 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002445
2446 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002447 for event_queue in self.event_queues:
2448 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002449
Clark Boylanb640e052014-04-03 16:41:46 -07002450 def waitUntilSettled(self):
2451 self.log.debug("Waiting until settled...")
2452 start = time.time()
2453 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002454 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002455 self.log.error("Timeout waiting for Zuul to settle")
2456 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002457 for event_queue in self.event_queues:
2458 self.log.error(" %s: %s" %
2459 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002460 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002461 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002462 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002463 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002464 self.log.error("All requests completed: %s" %
2465 (self.areAllNodeRequestsComplete(),))
2466 self.log.error("Merge client jobs: %s" %
2467 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002468 raise Exception("Timeout waiting for Zuul to settle")
2469 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002470
Paul Belanger174a8272017-03-14 13:20:10 -04002471 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002472 # have all build states propogated to zuul?
2473 if self.haveAllBuildsReported():
2474 # Join ensures that the queue is empty _and_ events have been
2475 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002476 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002477 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002478 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002479 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002480 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002481 self.areAllNodeRequestsComplete() and
2482 all(self.eventQueuesEmpty())):
2483 # The queue empty check is placed at the end to
2484 # ensure that if a component adds an event between
2485 # when locked the run handler and checked that the
2486 # components were stable, we don't erroneously
2487 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002488 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002489 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002490 self.log.debug("...settled.")
2491 return
2492 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002493 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002494 self.sched.wake_event.wait(0.1)
2495
2496 def countJobResults(self, jobs, result):
2497 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002498 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002499
Monty Taylor0d926122017-05-24 08:07:56 -05002500 def getBuildByName(self, name):
2501 for build in self.builds:
2502 if build.name == name:
2503 return build
2504 raise Exception("Unable to find build %s" % name)
2505
James E. Blair96c6bf82016-01-15 16:20:40 -08002506 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002507 for job in self.history:
2508 if (job.name == name and
2509 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002510 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002511 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002512 raise Exception("Unable to find job %s in history" % name)
2513
2514 def assertEmptyQueues(self):
2515 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002516 for tenant in self.sched.abide.tenants.values():
2517 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002518 for pipeline_queue in pipeline.queues:
2519 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002520 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002521 pipeline.name, pipeline_queue.name,
2522 pipeline_queue.queue))
2523 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002524 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002525
2526 def assertReportedStat(self, key, value=None, kind=None):
2527 start = time.time()
2528 while time.time() < (start + 5):
2529 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002530 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002531 if key == k:
2532 if value is None and kind is None:
2533 return
2534 elif value:
2535 if value == v:
2536 return
2537 elif kind:
2538 if v.endswith('|' + kind):
2539 return
2540 time.sleep(0.1)
2541
Clark Boylanb640e052014-04-03 16:41:46 -07002542 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002543
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002544 def assertBuilds(self, builds):
2545 """Assert that the running builds are as described.
2546
2547 The list of running builds is examined and must match exactly
2548 the list of builds described by the input.
2549
2550 :arg list builds: A list of dictionaries. Each item in the
2551 list must match the corresponding build in the build
2552 history, and each element of the dictionary must match the
2553 corresponding attribute of the build.
2554
2555 """
James E. Blair3158e282016-08-19 09:34:11 -07002556 try:
2557 self.assertEqual(len(self.builds), len(builds))
2558 for i, d in enumerate(builds):
2559 for k, v in d.items():
2560 self.assertEqual(
2561 getattr(self.builds[i], k), v,
2562 "Element %i in builds does not match" % (i,))
2563 except Exception:
2564 for build in self.builds:
2565 self.log.error("Running build: %s" % build)
2566 else:
2567 self.log.error("No running builds")
2568 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002569
James E. Blairb536ecc2016-08-31 10:11:42 -07002570 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002571 """Assert that the completed builds are as described.
2572
2573 The list of completed builds is examined and must match
2574 exactly the list of builds described by the input.
2575
2576 :arg list history: A list of dictionaries. Each item in the
2577 list must match the corresponding build in the build
2578 history, and each element of the dictionary must match the
2579 corresponding attribute of the build.
2580
James E. Blairb536ecc2016-08-31 10:11:42 -07002581 :arg bool ordered: If true, the history must match the order
2582 supplied, if false, the builds are permitted to have
2583 arrived in any order.
2584
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002585 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002586 def matches(history_item, item):
2587 for k, v in item.items():
2588 if getattr(history_item, k) != v:
2589 return False
2590 return True
James E. Blair3158e282016-08-19 09:34:11 -07002591 try:
2592 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002593 if ordered:
2594 for i, d in enumerate(history):
2595 if not matches(self.history[i], d):
2596 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002597 "Element %i in history does not match %s" %
2598 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002599 else:
2600 unseen = self.history[:]
2601 for i, d in enumerate(history):
2602 found = False
2603 for unseen_item in unseen:
2604 if matches(unseen_item, d):
2605 found = True
2606 unseen.remove(unseen_item)
2607 break
2608 if not found:
2609 raise Exception("No match found for element %i "
2610 "in history" % (i,))
2611 if unseen:
2612 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002613 except Exception:
2614 for build in self.history:
2615 self.log.error("Completed build: %s" % build)
2616 else:
2617 self.log.error("No completed builds")
2618 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002619
James E. Blair6ac368c2016-12-22 18:07:20 -08002620 def printHistory(self):
2621 """Log the build history.
2622
2623 This can be useful during tests to summarize what jobs have
2624 completed.
2625
2626 """
2627 self.log.debug("Build history:")
2628 for build in self.history:
2629 self.log.debug(build)
2630
James E. Blair59fdbac2015-12-07 17:08:06 -08002631 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002632 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2633
James E. Blair9ea70072017-04-19 16:05:30 -07002634 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002635 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002636 if not os.path.exists(root):
2637 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002638 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2639 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002640- tenant:
2641 name: openstack
2642 source:
2643 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002644 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002645 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002646 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002647 - org/project
2648 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002649 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002650 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002651 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002652 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002653 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002654
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002655 def addCommitToRepo(self, project, message, files,
2656 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002657 path = os.path.join(self.upstream_root, project)
2658 repo = git.Repo(path)
2659 repo.head.reference = branch
2660 zuul.merger.merger.reset_repo_to_head(repo)
2661 for fn, content in files.items():
2662 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002663 try:
2664 os.makedirs(os.path.dirname(fn))
2665 except OSError:
2666 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002667 with open(fn, 'w') as f:
2668 f.write(content)
2669 repo.index.add([fn])
2670 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002671 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002672 repo.heads[branch].commit = commit
2673 repo.head.reference = branch
2674 repo.git.clean('-x', '-f', '-d')
2675 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002676 if tag:
2677 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002678 return before
2679
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002680 def commitConfigUpdate(self, project_name, source_name):
2681 """Commit an update to zuul.yaml
2682
2683 This overwrites the zuul.yaml in the specificed project with
2684 the contents specified.
2685
2686 :arg str project_name: The name of the project containing
2687 zuul.yaml (e.g., common-config)
2688
2689 :arg str source_name: The path to the file (underneath the
2690 test fixture directory) whose contents should be used to
2691 replace zuul.yaml.
2692 """
2693
2694 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002695 files = {}
2696 with open(source_path, 'r') as f:
2697 data = f.read()
2698 layout = yaml.safe_load(data)
2699 files['zuul.yaml'] = data
2700 for item in layout:
2701 if 'job' in item:
2702 jobname = item['job']['name']
2703 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002704 before = self.addCommitToRepo(
2705 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002706 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002707 return before
2708
James E. Blair7fc8daa2016-08-08 15:37:15 -07002709 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002710
James E. Blair7fc8daa2016-08-08 15:37:15 -07002711 """Inject a Fake (Gerrit) event.
2712
2713 This method accepts a JSON-encoded event and simulates Zuul
2714 having received it from Gerrit. It could (and should)
2715 eventually apply to any connection type, but is currently only
2716 used with Gerrit connections. The name of the connection is
2717 used to look up the corresponding server, and the event is
2718 simulated as having been received by all Zuul connections
2719 attached to that server. So if two Gerrit connections in Zuul
2720 are connected to the same Gerrit server, and you invoke this
2721 method specifying the name of one of them, the event will be
2722 received by both.
2723
2724 .. note::
2725
2726 "self.fake_gerrit.addEvent" calls should be migrated to
2727 this method.
2728
2729 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002730 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002731 :arg str event: The JSON-encoded event.
2732
2733 """
2734 specified_conn = self.connections.connections[connection]
2735 for conn in self.connections.connections.values():
2736 if (isinstance(conn, specified_conn.__class__) and
2737 specified_conn.server == conn.server):
2738 conn.addEvent(event)
2739
James E. Blaird8af5422017-05-24 13:59:40 -07002740 def getUpstreamRepos(self, projects):
2741 """Return upstream git repo objects for the listed projects
2742
2743 :arg list projects: A list of strings, each the canonical name
2744 of a project.
2745
2746 :returns: A dictionary of {name: repo} for every listed
2747 project.
2748 :rtype: dict
2749
2750 """
2751
2752 repos = {}
2753 for project in projects:
2754 # FIXME(jeblair): the upstream root does not yet have a
2755 # hostname component; that needs to be added, and this
2756 # line removed:
2757 tmp_project_name = '/'.join(project.split('/')[1:])
2758 path = os.path.join(self.upstream_root, tmp_project_name)
2759 repo = git.Repo(path)
2760 repos[project] = repo
2761 return repos
2762
James E. Blair3f876d52016-07-22 13:07:14 -07002763
2764class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002765 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002766 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002767
Jamie Lennox7655b552017-03-17 12:33:38 +11002768 @contextmanager
2769 def jobLog(self, build):
2770 """Print job logs on assertion errors
2771
2772 This method is a context manager which, if it encounters an
2773 ecxeption, adds the build log to the debug output.
2774
2775 :arg Build build: The build that's being asserted.
2776 """
2777 try:
2778 yield
2779 except Exception:
2780 path = os.path.join(self.test_root, build.uuid,
2781 'work', 'logs', 'job-output.txt')
2782 with open(path) as f:
2783 self.log.debug(f.read())
2784 raise
2785
Joshua Heskethd78b4482015-09-14 16:56:34 -06002786
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002787class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002788 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002789 use_ssl = True
2790
2791
Joshua Heskethd78b4482015-09-14 16:56:34 -06002792class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002793 def setup_config(self):
2794 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002795 for section_name in self.config.sections():
2796 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2797 section_name, re.I)
2798 if not con_match:
2799 continue
2800
2801 if self.config.get(section_name, 'driver') == 'sql':
2802 f = MySQLSchemaFixture()
2803 self.useFixture(f)
2804 if (self.config.get(section_name, 'dburi') ==
2805 '$MYSQL_FIXTURE_DBURI$'):
2806 self.config.set(section_name, 'dburi', f.dburi)