blob: c08a49e7f7210310a7099ddb0936ef9a41fd810f [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')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700921 headers = {'X-Github-Event': name}
922 req = urllib.request.Request(
923 'http://localhost:%s/connection/%s/payload'
924 % (port, self.connection_name),
925 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000926 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700927
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200928 def getPull(self, project, number):
929 pr = self.pull_requests[number - 1]
930 data = {
931 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100932 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200933 'updated_at': pr.updated_at,
934 'base': {
935 'repo': {
936 'full_name': pr.project
937 },
938 'ref': pr.branch,
939 },
Jan Hruban37615e52015-11-19 14:30:49 +0100940 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700941 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200942 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800943 'sha': pr.head_sha,
944 'repo': {
945 'full_name': pr.project
946 }
Jesse Keating61040e72017-06-08 15:08:27 -0700947 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700948 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700949 'labels': pr.labels,
950 'merged': pr.is_merged,
951 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200952 }
953 return data
954
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800955 def getPullBySha(self, sha):
956 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
957 if len(prs) > 1:
958 raise Exception('Multiple pulls found with head sha: %s' % sha)
959 pr = prs[0]
960 return self.getPull(pr.project, pr.number)
961
Jesse Keatingae4cd272017-01-30 17:10:44 -0800962 def _getPullReviews(self, owner, project, number):
963 pr = self.pull_requests[number - 1]
964 return pr.reviews
965
Jan Hruban3b415922016-02-03 13:10:22 +0100966 def getUser(self, login):
967 data = {
968 'username': login,
969 'name': 'Github User',
970 'email': 'github.user@example.com'
971 }
972 return data
973
Jesse Keatingae4cd272017-01-30 17:10:44 -0800974 def getRepoPermission(self, project, login):
975 owner, proj = project.split('/')
976 for pr in self.pull_requests:
977 pr_owner, pr_project = pr.project.split('/')
978 if (pr_owner == owner and proj == pr_project):
979 if login in pr.writers:
980 return 'write'
981 else:
982 return 'read'
983
Gregory Haynes4fc12542015-04-22 20:38:06 -0700984 def getGitUrl(self, project):
985 return os.path.join(self.upstream_root, str(project))
986
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200987 def real_getGitUrl(self, project):
988 return super(FakeGithubConnection, self).getGitUrl(project)
989
Gregory Haynes4fc12542015-04-22 20:38:06 -0700990 def getProjectBranches(self, project):
991 """Masks getProjectBranches since we don't have a real github"""
992
993 # just returns master for now
994 return ['master']
995
Jan Hrubane252a732017-01-03 15:03:09 +0100996 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +0100997 # record that this got reported
998 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -0700999 pull_request = self.pull_requests[pr_number - 1]
1000 pull_request.addComment(message)
1001
Jan Hruban3b415922016-02-03 13:10:22 +01001002 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001003 # record that this got reported
1004 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001005 pull_request = self.pull_requests[pr_number - 1]
1006 if self.merge_failure:
1007 raise Exception('Pull request was not merged')
1008 if self.merge_not_allowed_count > 0:
1009 self.merge_not_allowed_count -= 1
1010 raise MergeFailure('Merge was not successful due to mergeability'
1011 ' conflict')
1012 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001013 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001014
Jesse Keatingd96e5882017-01-19 13:55:50 -08001015 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001016 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001017
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001018 def setCommitStatus(self, project, sha, state, url='', description='',
1019 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001020 # record that this got reported
1021 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001022 # always insert a status to the front of the list, to represent
1023 # the last status provided for a commit.
1024 # Since we're bypassing github API, which would require a user, we
1025 # default the user as 'zuul' here.
1026 self.statuses.setdefault(project, {}).setdefault(sha, [])
1027 self.statuses[project][sha].insert(0, {
1028 'state': state,
1029 'url': url,
1030 'description': description,
1031 'context': context,
1032 'creator': {
1033 'login': user
1034 }
1035 })
Jan Hrubane252a732017-01-03 15:03:09 +01001036
Jan Hruban16ad31f2015-11-07 14:39:07 +01001037 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001038 # record that this got reported
1039 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001040 pull_request = self.pull_requests[pr_number - 1]
1041 pull_request.addLabel(label)
1042
1043 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001044 # record that this got reported
1045 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001046 pull_request = self.pull_requests[pr_number - 1]
1047 pull_request.removeLabel(label)
1048
Jesse Keatinga41566f2017-06-14 18:17:51 -07001049 def _getNeededByFromPR(self, change):
1050 prs = []
1051 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001052 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001053 change.number))
1054 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001055 if not pr.body:
1056 body = ''
1057 else:
1058 body = pr.body
1059 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001060 # Get our version of a pull so that it's a dict
1061 pull = self.getPull(pr.project, pr.number)
1062 prs.append(pull)
1063
1064 return prs
1065
Gregory Haynes4fc12542015-04-22 20:38:06 -07001066
Clark Boylanb640e052014-04-03 16:41:46 -07001067class BuildHistory(object):
1068 def __init__(self, **kw):
1069 self.__dict__.update(kw)
1070
1071 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001072 return ("<Completed build, result: %s name: %s uuid: %s "
1073 "changes: %s ref: %s>" %
1074 (self.result, self.name, self.uuid,
1075 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001076
1077
Clark Boylanb640e052014-04-03 16:41:46 -07001078class FakeStatsd(threading.Thread):
1079 def __init__(self):
1080 threading.Thread.__init__(self)
1081 self.daemon = True
1082 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1083 self.sock.bind(('', 0))
1084 self.port = self.sock.getsockname()[1]
1085 self.wake_read, self.wake_write = os.pipe()
1086 self.stats = []
1087
1088 def run(self):
1089 while True:
1090 poll = select.poll()
1091 poll.register(self.sock, select.POLLIN)
1092 poll.register(self.wake_read, select.POLLIN)
1093 ret = poll.poll()
1094 for (fd, event) in ret:
1095 if fd == self.sock.fileno():
1096 data = self.sock.recvfrom(1024)
1097 if not data:
1098 return
1099 self.stats.append(data[0])
1100 if fd == self.wake_read:
1101 return
1102
1103 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001104 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001105
1106
James E. Blaire1767bc2016-08-02 10:00:27 -07001107class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001108 log = logging.getLogger("zuul.test")
1109
Paul Belanger174a8272017-03-14 13:20:10 -04001110 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001111 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001112 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001113 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001114 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001115 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001116 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001117 # TODOv3(jeblair): self.node is really "the label of the node
1118 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001119 # keep using it like this, or we may end up exposing more of
1120 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001121 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001122 self.node = None
1123 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001124 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001125 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001126 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001127 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001128 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001129 self.wait_condition = threading.Condition()
1130 self.waiting = False
1131 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001132 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001133 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001134 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001135 items = self.parameters['zuul']['items']
1136 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1137 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001138
James E. Blair3158e282016-08-19 09:34:11 -07001139 def __repr__(self):
1140 waiting = ''
1141 if self.waiting:
1142 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001143 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1144 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001145
Clark Boylanb640e052014-04-03 16:41:46 -07001146 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001147 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001148 self.wait_condition.acquire()
1149 self.wait_condition.notify()
1150 self.waiting = False
1151 self.log.debug("Build %s released" % self.unique)
1152 self.wait_condition.release()
1153
1154 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001155 """Return whether this build is being held.
1156
1157 :returns: Whether the build is being held.
1158 :rtype: bool
1159 """
1160
Clark Boylanb640e052014-04-03 16:41:46 -07001161 self.wait_condition.acquire()
1162 if self.waiting:
1163 ret = True
1164 else:
1165 ret = False
1166 self.wait_condition.release()
1167 return ret
1168
1169 def _wait(self):
1170 self.wait_condition.acquire()
1171 self.waiting = True
1172 self.log.debug("Build %s waiting" % self.unique)
1173 self.wait_condition.wait()
1174 self.wait_condition.release()
1175
1176 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001177 self.log.debug('Running build %s' % self.unique)
1178
Paul Belanger174a8272017-03-14 13:20:10 -04001179 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001180 self.log.debug('Holding build %s' % self.unique)
1181 self._wait()
1182 self.log.debug("Build %s continuing" % self.unique)
1183
James E. Blair412fba82017-01-26 15:00:50 -08001184 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001185 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001186 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001187 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001188 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001189 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001190 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001191
James E. Blaire1767bc2016-08-02 10:00:27 -07001192 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001193
James E. Blaira5dba232016-08-08 15:53:24 -07001194 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001195 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001196 for change in changes:
1197 if self.hasChanges(change):
1198 return True
1199 return False
1200
James E. Blaire7b99a02016-08-05 14:27:34 -07001201 def hasChanges(self, *changes):
1202 """Return whether this build has certain changes in its git repos.
1203
1204 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001205 are expected to be present (in order) in the git repository of
1206 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001207
1208 :returns: Whether the build has the indicated changes.
1209 :rtype: bool
1210
1211 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001212 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001213 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001214 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001215 try:
1216 repo = git.Repo(path)
1217 except NoSuchPathError as e:
1218 self.log.debug('%s' % e)
1219 return False
James E. Blair247cab72017-07-20 16:52:36 -07001220 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001221 commit_message = '%s-1' % change.subject
1222 self.log.debug("Checking if build %s has changes; commit_message "
1223 "%s; repo_messages %s" % (self, commit_message,
1224 repo_messages))
1225 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001226 self.log.debug(" messages do not match")
1227 return False
1228 self.log.debug(" OK")
1229 return True
1230
James E. Blaird8af5422017-05-24 13:59:40 -07001231 def getWorkspaceRepos(self, projects):
1232 """Return workspace git repo objects for the listed projects
1233
1234 :arg list projects: A list of strings, each the canonical name
1235 of a project.
1236
1237 :returns: A dictionary of {name: repo} for every listed
1238 project.
1239 :rtype: dict
1240
1241 """
1242
1243 repos = {}
1244 for project in projects:
1245 path = os.path.join(self.jobdir.src_root, project)
1246 repo = git.Repo(path)
1247 repos[project] = repo
1248 return repos
1249
Clark Boylanb640e052014-04-03 16:41:46 -07001250
Paul Belanger174a8272017-03-14 13:20:10 -04001251class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1252 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001253
Paul Belanger174a8272017-03-14 13:20:10 -04001254 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001255 they will report that they have started but then pause until
1256 released before reporting completion. This attribute may be
1257 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001258 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001259 be explicitly released.
1260
1261 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001262 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001263 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001264 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001265 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001266 self.hold_jobs_in_build = False
1267 self.lock = threading.Lock()
1268 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001269 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001270 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001271 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001272
James E. Blaira5dba232016-08-08 15:53:24 -07001273 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001274 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001275
1276 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001277 :arg Change change: The :py:class:`~tests.base.FakeChange`
1278 instance which should cause the job to fail. This job
1279 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001280
1281 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001282 l = self.fail_tests.get(name, [])
1283 l.append(change)
1284 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001285
James E. Blair962220f2016-08-03 11:22:38 -07001286 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001287 """Release a held build.
1288
1289 :arg str regex: A regular expression which, if supplied, will
1290 cause only builds with matching names to be released. If
1291 not supplied, all builds will be released.
1292
1293 """
James E. Blair962220f2016-08-03 11:22:38 -07001294 builds = self.running_builds[:]
1295 self.log.debug("Releasing build %s (%s)" % (regex,
1296 len(self.running_builds)))
1297 for build in builds:
1298 if not regex or re.match(regex, build.name):
1299 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001300 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001301 build.release()
1302 else:
1303 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001304 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001305 self.log.debug("Done releasing builds %s (%s)" %
1306 (regex, len(self.running_builds)))
1307
Paul Belanger174a8272017-03-14 13:20:10 -04001308 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001309 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001310 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001311 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001312 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001313 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001314 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001315 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001316 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1317 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001318
1319 def stopJob(self, job):
1320 self.log.debug("handle stop")
1321 parameters = json.loads(job.arguments)
1322 uuid = parameters['uuid']
1323 for build in self.running_builds:
1324 if build.unique == uuid:
1325 build.aborted = True
1326 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001327 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001328
James E. Blaira002b032017-04-18 10:35:48 -07001329 def stop(self):
1330 for build in self.running_builds:
1331 build.release()
1332 super(RecordingExecutorServer, self).stop()
1333
Joshua Hesketh50c21782016-10-13 21:34:14 +11001334
Paul Belanger174a8272017-03-14 13:20:10 -04001335class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001336 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001337 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001338 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001339 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001340 if not commit: # merge conflict
1341 self.recordResult('MERGER_FAILURE')
1342 return commit
1343
1344 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001345 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001346 self.executor_server.lock.acquire()
1347 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001348 BuildHistory(name=build.name, result=result, changes=build.changes,
1349 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001350 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001351 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001352 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001353 )
Paul Belanger174a8272017-03-14 13:20:10 -04001354 self.executor_server.running_builds.remove(build)
1355 del self.executor_server.job_builds[self.job.unique]
1356 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001357
1358 def runPlaybooks(self, args):
1359 build = self.executor_server.job_builds[self.job.unique]
1360 build.jobdir = self.jobdir
1361
1362 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1363 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001364 return result
1365
James E. Blair74a82cf2017-07-12 17:23:08 -07001366 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001367 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001368
Paul Belanger174a8272017-03-14 13:20:10 -04001369 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001370 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001371 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001372 else:
1373 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001374 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001375
James E. Blairad8dca02017-02-21 11:48:32 -05001376 def getHostList(self, args):
1377 self.log.debug("hostlist")
1378 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001379 for host in hosts:
1380 host['host_vars']['ansible_connection'] = 'local'
1381
1382 hosts.append(dict(
1383 name='localhost',
1384 host_vars=dict(ansible_connection='local'),
1385 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001386 return hosts
1387
James E. Blairf5dbd002015-12-23 15:26:17 -08001388
Clark Boylanb640e052014-04-03 16:41:46 -07001389class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001390 """A Gearman server for use in tests.
1391
1392 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1393 added to the queue but will not be distributed to workers
1394 until released. This attribute may be changed at any time and
1395 will take effect for subsequently enqueued jobs, but
1396 previously held jobs will still need to be explicitly
1397 released.
1398
1399 """
1400
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001401 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001402 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001403 if use_ssl:
1404 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1405 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1406 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1407 else:
1408 ssl_ca = None
1409 ssl_cert = None
1410 ssl_key = None
1411
1412 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1413 ssl_cert=ssl_cert,
1414 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001415
1416 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001417 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1418 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001419 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001420 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001421 job.waiting = self.hold_jobs_in_queue
1422 else:
1423 job.waiting = False
1424 if job.waiting:
1425 continue
1426 if job.name in connection.functions:
1427 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001428 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001429 connection.related_jobs[job.handle] = job
1430 job.worker_connection = connection
1431 job.running = True
1432 return job
1433 return None
1434
1435 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001436 """Release a held job.
1437
1438 :arg str regex: A regular expression which, if supplied, will
1439 cause only jobs with matching names to be released. If
1440 not supplied, all jobs will be released.
1441 """
Clark Boylanb640e052014-04-03 16:41:46 -07001442 released = False
1443 qlen = (len(self.high_queue) + len(self.normal_queue) +
1444 len(self.low_queue))
1445 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1446 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001447 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001448 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001449 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001450 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001451 self.log.debug("releasing queued job %s" %
1452 job.unique)
1453 job.waiting = False
1454 released = True
1455 else:
1456 self.log.debug("not releasing queued job %s" %
1457 job.unique)
1458 if released:
1459 self.wakeConnections()
1460 qlen = (len(self.high_queue) + len(self.normal_queue) +
1461 len(self.low_queue))
1462 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1463
1464
1465class FakeSMTP(object):
1466 log = logging.getLogger('zuul.FakeSMTP')
1467
1468 def __init__(self, messages, server, port):
1469 self.server = server
1470 self.port = port
1471 self.messages = messages
1472
1473 def sendmail(self, from_email, to_email, msg):
1474 self.log.info("Sending email from %s, to %s, with msg %s" % (
1475 from_email, to_email, msg))
1476
1477 headers = msg.split('\n\n', 1)[0]
1478 body = msg.split('\n\n', 1)[1]
1479
1480 self.messages.append(dict(
1481 from_email=from_email,
1482 to_email=to_email,
1483 msg=msg,
1484 headers=headers,
1485 body=body,
1486 ))
1487
1488 return True
1489
1490 def quit(self):
1491 return True
1492
1493
James E. Blairdce6cea2016-12-20 16:45:32 -08001494class FakeNodepool(object):
1495 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001496 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001497
1498 log = logging.getLogger("zuul.test.FakeNodepool")
1499
1500 def __init__(self, host, port, chroot):
1501 self.client = kazoo.client.KazooClient(
1502 hosts='%s:%s%s' % (host, port, chroot))
1503 self.client.start()
1504 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001505 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001506 self.thread = threading.Thread(target=self.run)
1507 self.thread.daemon = True
1508 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001509 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001510
1511 def stop(self):
1512 self._running = False
1513 self.thread.join()
1514 self.client.stop()
1515 self.client.close()
1516
1517 def run(self):
1518 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001519 try:
1520 self._run()
1521 except Exception:
1522 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001523 time.sleep(0.1)
1524
1525 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001526 if self.paused:
1527 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001528 for req in self.getNodeRequests():
1529 self.fulfillRequest(req)
1530
1531 def getNodeRequests(self):
1532 try:
1533 reqids = self.client.get_children(self.REQUEST_ROOT)
1534 except kazoo.exceptions.NoNodeError:
1535 return []
1536 reqs = []
1537 for oid in sorted(reqids):
1538 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001539 try:
1540 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001541 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001542 data['_oid'] = oid
1543 reqs.append(data)
1544 except kazoo.exceptions.NoNodeError:
1545 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001546 return reqs
1547
James E. Blaire18d4602017-01-05 11:17:28 -08001548 def getNodes(self):
1549 try:
1550 nodeids = self.client.get_children(self.NODE_ROOT)
1551 except kazoo.exceptions.NoNodeError:
1552 return []
1553 nodes = []
1554 for oid in sorted(nodeids):
1555 path = self.NODE_ROOT + '/' + oid
1556 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001557 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001558 data['_oid'] = oid
1559 try:
1560 lockfiles = self.client.get_children(path + '/lock')
1561 except kazoo.exceptions.NoNodeError:
1562 lockfiles = []
1563 if lockfiles:
1564 data['_lock'] = True
1565 else:
1566 data['_lock'] = False
1567 nodes.append(data)
1568 return nodes
1569
James E. Blaira38c28e2017-01-04 10:33:20 -08001570 def makeNode(self, request_id, node_type):
1571 now = time.time()
1572 path = '/nodepool/nodes/'
1573 data = dict(type=node_type,
1574 provider='test-provider',
1575 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001576 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001577 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001578 public_ipv4='127.0.0.1',
1579 private_ipv4=None,
1580 public_ipv6=None,
1581 allocated_to=request_id,
1582 state='ready',
1583 state_time=now,
1584 created_time=now,
1585 updated_time=now,
1586 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001587 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001588 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001589 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001590 path = self.client.create(path, data,
1591 makepath=True,
1592 sequence=True)
1593 nodeid = path.split("/")[-1]
1594 return nodeid
1595
James E. Blair6ab79e02017-01-06 10:10:17 -08001596 def addFailRequest(self, request):
1597 self.fail_requests.add(request['_oid'])
1598
James E. Blairdce6cea2016-12-20 16:45:32 -08001599 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001600 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001601 return
1602 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001603 oid = request['_oid']
1604 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001605
James E. Blair6ab79e02017-01-06 10:10:17 -08001606 if oid in self.fail_requests:
1607 request['state'] = 'failed'
1608 else:
1609 request['state'] = 'fulfilled'
1610 nodes = []
1611 for node in request['node_types']:
1612 nodeid = self.makeNode(oid, node)
1613 nodes.append(nodeid)
1614 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001615
James E. Blaira38c28e2017-01-04 10:33:20 -08001616 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001617 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001618 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001619 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001620 try:
1621 self.client.set(path, data)
1622 except kazoo.exceptions.NoNodeError:
1623 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001624
1625
James E. Blair498059b2016-12-20 13:50:13 -08001626class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001627 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001628 super(ChrootedKazooFixture, self).__init__()
1629
1630 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1631 if ':' in zk_host:
1632 host, port = zk_host.split(':')
1633 else:
1634 host = zk_host
1635 port = None
1636
1637 self.zookeeper_host = host
1638
1639 if not port:
1640 self.zookeeper_port = 2181
1641 else:
1642 self.zookeeper_port = int(port)
1643
Clark Boylan621ec9a2017-04-07 17:41:33 -07001644 self.test_id = test_id
1645
James E. Blair498059b2016-12-20 13:50:13 -08001646 def _setUp(self):
1647 # Make sure the test chroot paths do not conflict
1648 random_bits = ''.join(random.choice(string.ascii_lowercase +
1649 string.ascii_uppercase)
1650 for x in range(8))
1651
Clark Boylan621ec9a2017-04-07 17:41:33 -07001652 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001653 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1654
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001655 self.addCleanup(self._cleanup)
1656
James E. Blair498059b2016-12-20 13:50:13 -08001657 # Ensure the chroot path exists and clean up any pre-existing znodes.
1658 _tmp_client = kazoo.client.KazooClient(
1659 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1660 _tmp_client.start()
1661
1662 if _tmp_client.exists(self.zookeeper_chroot):
1663 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1664
1665 _tmp_client.ensure_path(self.zookeeper_chroot)
1666 _tmp_client.stop()
1667 _tmp_client.close()
1668
James E. Blair498059b2016-12-20 13:50:13 -08001669 def _cleanup(self):
1670 '''Remove the chroot path.'''
1671 # Need a non-chroot'ed client to remove the chroot path
1672 _tmp_client = kazoo.client.KazooClient(
1673 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1674 _tmp_client.start()
1675 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1676 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001677 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001678
1679
Joshua Heskethd78b4482015-09-14 16:56:34 -06001680class MySQLSchemaFixture(fixtures.Fixture):
1681 def setUp(self):
1682 super(MySQLSchemaFixture, self).setUp()
1683
1684 random_bits = ''.join(random.choice(string.ascii_lowercase +
1685 string.ascii_uppercase)
1686 for x in range(8))
1687 self.name = '%s_%s' % (random_bits, os.getpid())
1688 self.passwd = uuid.uuid4().hex
1689 db = pymysql.connect(host="localhost",
1690 user="openstack_citest",
1691 passwd="openstack_citest",
1692 db="openstack_citest")
1693 cur = db.cursor()
1694 cur.execute("create database %s" % self.name)
1695 cur.execute(
1696 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1697 (self.name, self.name, self.passwd))
1698 cur.execute("flush privileges")
1699
1700 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1701 self.passwd,
1702 self.name)
1703 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1704 self.addCleanup(self.cleanup)
1705
1706 def cleanup(self):
1707 db = pymysql.connect(host="localhost",
1708 user="openstack_citest",
1709 passwd="openstack_citest",
1710 db="openstack_citest")
1711 cur = db.cursor()
1712 cur.execute("drop database %s" % self.name)
1713 cur.execute("drop user '%s'@'localhost'" % self.name)
1714 cur.execute("flush privileges")
1715
1716
Maru Newby3fe5f852015-01-13 04:22:14 +00001717class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001718 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001719 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001720
James E. Blair1c236df2017-02-01 14:07:24 -08001721 def attachLogs(self, *args):
1722 def reader():
1723 self._log_stream.seek(0)
1724 while True:
1725 x = self._log_stream.read(4096)
1726 if not x:
1727 break
1728 yield x.encode('utf8')
1729 content = testtools.content.content_from_reader(
1730 reader,
1731 testtools.content_type.UTF8_TEXT,
1732 False)
1733 self.addDetail('logging', content)
1734
Clark Boylanb640e052014-04-03 16:41:46 -07001735 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001736 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001737 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1738 try:
1739 test_timeout = int(test_timeout)
1740 except ValueError:
1741 # If timeout value is invalid do not set a timeout.
1742 test_timeout = 0
1743 if test_timeout > 0:
1744 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1745
1746 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1747 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1748 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1749 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1750 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1751 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1752 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1753 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1754 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1755 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001756 self._log_stream = StringIO()
1757 self.addOnException(self.attachLogs)
1758 else:
1759 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001760
James E. Blair73b41772017-05-22 13:22:55 -07001761 # NOTE(jeblair): this is temporary extra debugging to try to
1762 # track down a possible leak.
1763 orig_git_repo_init = git.Repo.__init__
1764
1765 def git_repo_init(myself, *args, **kw):
1766 orig_git_repo_init(myself, *args, **kw)
1767 self.log.debug("Created git repo 0x%x %s" %
1768 (id(myself), repr(myself)))
1769
1770 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1771 git_repo_init))
1772
James E. Blair1c236df2017-02-01 14:07:24 -08001773 handler = logging.StreamHandler(self._log_stream)
1774 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1775 '%(levelname)-8s %(message)s')
1776 handler.setFormatter(formatter)
1777
1778 logger = logging.getLogger()
1779 logger.setLevel(logging.DEBUG)
1780 logger.addHandler(handler)
1781
Clark Boylan3410d532017-04-25 12:35:29 -07001782 # Make sure we don't carry old handlers around in process state
1783 # which slows down test runs
1784 self.addCleanup(logger.removeHandler, handler)
1785 self.addCleanup(handler.close)
1786 self.addCleanup(handler.flush)
1787
James E. Blair1c236df2017-02-01 14:07:24 -08001788 # NOTE(notmorgan): Extract logging overrides for specific
1789 # libraries from the OS_LOG_DEFAULTS env and create loggers
1790 # for each. This is used to limit the output during test runs
1791 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001792 log_defaults_from_env = os.environ.get(
1793 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001794 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001795
James E. Blairdce6cea2016-12-20 16:45:32 -08001796 if log_defaults_from_env:
1797 for default in log_defaults_from_env.split(','):
1798 try:
1799 name, level_str = default.split('=', 1)
1800 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001801 logger = logging.getLogger(name)
1802 logger.setLevel(level)
1803 logger.addHandler(handler)
1804 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001805 except ValueError:
1806 # NOTE(notmorgan): Invalid format of the log default,
1807 # skip and don't try and apply a logger for the
1808 # specified module
1809 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001810
Maru Newby3fe5f852015-01-13 04:22:14 +00001811
1812class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001813 """A test case with a functioning Zuul.
1814
1815 The following class variables are used during test setup and can
1816 be overidden by subclasses but are effectively read-only once a
1817 test method starts running:
1818
1819 :cvar str config_file: This points to the main zuul config file
1820 within the fixtures directory. Subclasses may override this
1821 to obtain a different behavior.
1822
1823 :cvar str tenant_config_file: This is the tenant config file
1824 (which specifies from what git repos the configuration should
1825 be loaded). It defaults to the value specified in
1826 `config_file` but can be overidden by subclasses to obtain a
1827 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001828 configuration. See also the :py:func:`simple_layout`
1829 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001830
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001831 :cvar bool create_project_keys: Indicates whether Zuul should
1832 auto-generate keys for each project, or whether the test
1833 infrastructure should insert dummy keys to save time during
1834 startup. Defaults to False.
1835
James E. Blaire7b99a02016-08-05 14:27:34 -07001836 The following are instance variables that are useful within test
1837 methods:
1838
1839 :ivar FakeGerritConnection fake_<connection>:
1840 A :py:class:`~tests.base.FakeGerritConnection` will be
1841 instantiated for each connection present in the config file
1842 and stored here. For instance, `fake_gerrit` will hold the
1843 FakeGerritConnection object for a connection named `gerrit`.
1844
1845 :ivar FakeGearmanServer gearman_server: An instance of
1846 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1847 server that all of the Zuul components in this test use to
1848 communicate with each other.
1849
Paul Belanger174a8272017-03-14 13:20:10 -04001850 :ivar RecordingExecutorServer executor_server: An instance of
1851 :py:class:`~tests.base.RecordingExecutorServer` which is the
1852 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001853
1854 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1855 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001856 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001857 list upon completion.
1858
1859 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1860 objects representing completed builds. They are appended to
1861 the list in the order they complete.
1862
1863 """
1864
James E. Blair83005782015-12-11 14:46:03 -08001865 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001866 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001867 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001868 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001869
1870 def _startMerger(self):
1871 self.merge_server = zuul.merger.server.MergeServer(self.config,
1872 self.connections)
1873 self.merge_server.start()
1874
Maru Newby3fe5f852015-01-13 04:22:14 +00001875 def setUp(self):
1876 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001877
1878 self.setupZK()
1879
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001880 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001881 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001882 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1883 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001884 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001885 tmp_root = tempfile.mkdtemp(
1886 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001887 self.test_root = os.path.join(tmp_root, "zuul-test")
1888 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001889 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001890 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001891 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001892 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1893 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001894
1895 if os.path.exists(self.test_root):
1896 shutil.rmtree(self.test_root)
1897 os.makedirs(self.test_root)
1898 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001899 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001900 os.makedirs(self.merger_state_root)
1901 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001902
1903 # Make per test copy of Configuration.
1904 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001905 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1906 if not os.path.exists(self.private_key_file):
1907 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1908 shutil.copy(src_private_key_file, self.private_key_file)
1909 shutil.copy('{}.pub'.format(src_private_key_file),
1910 '{}.pub'.format(self.private_key_file))
1911 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001912 self.config.set('scheduler', 'tenant_config',
1913 os.path.join(
1914 FIXTURE_DIR,
1915 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001916 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001917 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001918 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001919 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001920 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001921 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001922
Clark Boylanb640e052014-04-03 16:41:46 -07001923 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001924 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1925 # see: https://github.com/jsocol/pystatsd/issues/61
1926 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001927 os.environ['STATSD_PORT'] = str(self.statsd.port)
1928 self.statsd.start()
1929 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001930 importlib.reload(statsd)
1931 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001932
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001933 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001934
1935 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001936 self.log.info("Gearman server on port %s" %
1937 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001938 if self.use_ssl:
1939 self.log.info('SSL enabled for gearman')
1940 self.config.set(
1941 'gearman', 'ssl_ca',
1942 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1943 self.config.set(
1944 'gearman', 'ssl_cert',
1945 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1946 self.config.set(
1947 'gearman', 'ssl_key',
1948 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001949
James E. Blaire511d2f2016-12-08 15:22:26 -08001950 gerritsource.GerritSource.replication_timeout = 1.5
1951 gerritsource.GerritSource.replication_retry_interval = 0.5
1952 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001953
Joshua Hesketh352264b2015-08-11 23:42:08 +10001954 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001955
Jan Hruban7083edd2015-08-21 14:00:54 +02001956 self.webapp = zuul.webapp.WebApp(
1957 self.sched, port=0, listen_address='127.0.0.1')
1958
Jan Hruban6b71aff2015-10-22 16:58:08 +02001959 self.event_queues = [
1960 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001961 self.sched.trigger_event_queue,
1962 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001963 ]
1964
James E. Blairfef78942016-03-11 16:28:56 -08001965 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001966 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001967
Paul Belanger174a8272017-03-14 13:20:10 -04001968 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001969 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001970 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001971 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001972 _test_root=self.test_root,
1973 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001974 self.executor_server.start()
1975 self.history = self.executor_server.build_history
1976 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001977
Paul Belanger174a8272017-03-14 13:20:10 -04001978 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001979 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001980 self.merge_client = zuul.merger.client.MergeClient(
1981 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001982 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001983 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001984 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001985
James E. Blair0d5a36e2017-02-21 10:53:44 -05001986 self.fake_nodepool = FakeNodepool(
1987 self.zk_chroot_fixture.zookeeper_host,
1988 self.zk_chroot_fixture.zookeeper_port,
1989 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001990
Paul Belanger174a8272017-03-14 13:20:10 -04001991 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001992 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001993 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001994 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001995
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001996 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001997
1998 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001999 self.webapp.start()
2000 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002001 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002002 # Cleanups are run in reverse order
2003 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002004 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002005 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002006
James E. Blairb9c0d772017-03-03 14:34:49 -08002007 self.sched.reconfigure(self.config)
2008 self.sched.resume()
2009
Tobias Henkel7df274b2017-05-26 17:41:11 +02002010 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002011 # Set up gerrit related fakes
2012 # Set a changes database so multiple FakeGerrit's can report back to
2013 # a virtual canonical database given by the configured hostname
2014 self.gerrit_changes_dbs = {}
2015
2016 def getGerritConnection(driver, name, config):
2017 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2018 con = FakeGerritConnection(driver, name, config,
2019 changes_db=db,
2020 upstream_root=self.upstream_root)
2021 self.event_queues.append(con.event_queue)
2022 setattr(self, 'fake_' + name, con)
2023 return con
2024
2025 self.useFixture(fixtures.MonkeyPatch(
2026 'zuul.driver.gerrit.GerritDriver.getConnection',
2027 getGerritConnection))
2028
Gregory Haynes4fc12542015-04-22 20:38:06 -07002029 def getGithubConnection(driver, name, config):
2030 con = FakeGithubConnection(driver, name, config,
2031 upstream_root=self.upstream_root)
2032 setattr(self, 'fake_' + name, con)
2033 return con
2034
2035 self.useFixture(fixtures.MonkeyPatch(
2036 'zuul.driver.github.GithubDriver.getConnection',
2037 getGithubConnection))
2038
James E. Blaire511d2f2016-12-08 15:22:26 -08002039 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002040 # TODO(jhesketh): This should come from lib.connections for better
2041 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002042 # Register connections from the config
2043 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002044
Joshua Hesketh352264b2015-08-11 23:42:08 +10002045 def FakeSMTPFactory(*args, **kw):
2046 args = [self.smtp_messages] + list(args)
2047 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002048
Joshua Hesketh352264b2015-08-11 23:42:08 +10002049 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002050
James E. Blaire511d2f2016-12-08 15:22:26 -08002051 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002052 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002053 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002054
James E. Blair83005782015-12-11 14:46:03 -08002055 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002056 # This creates the per-test configuration object. It can be
2057 # overriden by subclasses, but should not need to be since it
2058 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002059 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002060 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002061
James E. Blair39840362017-06-23 20:34:02 +01002062 sections = ['zuul', 'scheduler', 'executor', 'merger']
2063 for section in sections:
2064 if not self.config.has_section(section):
2065 self.config.add_section(section)
2066
James E. Blair06cc3922017-04-19 10:08:10 -07002067 if not self.setupSimpleLayout():
2068 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002069 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002070 self.tenant_config_file)
2071 git_path = os.path.join(
2072 os.path.dirname(
2073 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2074 'git')
2075 if os.path.exists(git_path):
2076 for reponame in os.listdir(git_path):
2077 project = reponame.replace('_', '/')
2078 self.copyDirToRepo(project,
2079 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002080 # Make test_root persist after ansible run for .flag test
2081 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002082 self.setupAllProjectKeys()
2083
James E. Blair06cc3922017-04-19 10:08:10 -07002084 def setupSimpleLayout(self):
2085 # If the test method has been decorated with a simple_layout,
2086 # use that instead of the class tenant_config_file. Set up a
2087 # single config-project with the specified layout, and
2088 # initialize repos for all of the 'project' entries which
2089 # appear in the layout.
2090 test_name = self.id().split('.')[-1]
2091 test = getattr(self, test_name)
2092 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002093 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002094 else:
2095 return False
2096
James E. Blairb70e55a2017-04-19 12:57:02 -07002097 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002098 path = os.path.join(FIXTURE_DIR, path)
2099 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002100 data = f.read()
2101 layout = yaml.safe_load(data)
2102 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002103 untrusted_projects = []
2104 for item in layout:
2105 if 'project' in item:
2106 name = item['project']['name']
2107 untrusted_projects.append(name)
2108 self.init_repo(name)
2109 self.addCommitToRepo(name, 'initial commit',
2110 files={'README': ''},
2111 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002112 if 'job' in item:
2113 jobname = item['job']['name']
2114 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002115
2116 root = os.path.join(self.test_root, "config")
2117 if not os.path.exists(root):
2118 os.makedirs(root)
2119 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2120 config = [{'tenant':
2121 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002122 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002123 {'config-projects': ['common-config'],
2124 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002125 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002126 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002127 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002128 os.path.join(FIXTURE_DIR, f.name))
2129
2130 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002131 self.addCommitToRepo('common-config', 'add content from fixture',
2132 files, branch='master', tag='init')
2133
2134 return True
2135
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002136 def setupAllProjectKeys(self):
2137 if self.create_project_keys:
2138 return
2139
James E. Blair39840362017-06-23 20:34:02 +01002140 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002141 with open(os.path.join(FIXTURE_DIR, path)) as f:
2142 tenant_config = yaml.safe_load(f.read())
2143 for tenant in tenant_config:
2144 sources = tenant['tenant']['source']
2145 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002146 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002147 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002148 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002149 self.setupProjectKeys(source, project)
2150
2151 def setupProjectKeys(self, source, project):
2152 # Make sure we set up an RSA key for the project so that we
2153 # don't spend time generating one:
2154
James E. Blair6459db12017-06-29 14:57:20 -07002155 if isinstance(project, dict):
2156 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002157 key_root = os.path.join(self.state_root, 'keys')
2158 if not os.path.isdir(key_root):
2159 os.mkdir(key_root, 0o700)
2160 private_key_file = os.path.join(key_root, source, project + '.pem')
2161 private_key_dir = os.path.dirname(private_key_file)
2162 self.log.debug("Installing test keys for project %s at %s" % (
2163 project, private_key_file))
2164 if not os.path.isdir(private_key_dir):
2165 os.makedirs(private_key_dir)
2166 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2167 with open(private_key_file, 'w') as o:
2168 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002169
James E. Blair498059b2016-12-20 13:50:13 -08002170 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002171 self.zk_chroot_fixture = self.useFixture(
2172 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002173 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002174 self.zk_chroot_fixture.zookeeper_host,
2175 self.zk_chroot_fixture.zookeeper_port,
2176 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002177
James E. Blair96c6bf82016-01-15 16:20:40 -08002178 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002179 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002180
2181 files = {}
2182 for (dirpath, dirnames, filenames) in os.walk(source_path):
2183 for filename in filenames:
2184 test_tree_filepath = os.path.join(dirpath, filename)
2185 common_path = os.path.commonprefix([test_tree_filepath,
2186 source_path])
2187 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2188 with open(test_tree_filepath, 'r') as f:
2189 content = f.read()
2190 files[relative_filepath] = content
2191 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002192 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002193
James E. Blaire18d4602017-01-05 11:17:28 -08002194 def assertNodepoolState(self):
2195 # Make sure that there are no pending requests
2196
2197 requests = self.fake_nodepool.getNodeRequests()
2198 self.assertEqual(len(requests), 0)
2199
2200 nodes = self.fake_nodepool.getNodes()
2201 for node in nodes:
2202 self.assertFalse(node['_lock'], "Node %s is locked" %
2203 (node['_oid'],))
2204
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002205 def assertNoGeneratedKeys(self):
2206 # Make sure that Zuul did not generate any project keys
2207 # (unless it was supposed to).
2208
2209 if self.create_project_keys:
2210 return
2211
2212 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2213 test_key = i.read()
2214
2215 key_root = os.path.join(self.state_root, 'keys')
2216 for root, dirname, files in os.walk(key_root):
2217 for fn in files:
2218 with open(os.path.join(root, fn)) as f:
2219 self.assertEqual(test_key, f.read())
2220
Clark Boylanb640e052014-04-03 16:41:46 -07002221 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002222 self.log.debug("Assert final state")
2223 # Make sure no jobs are running
2224 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002225 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002226 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002227 gc.collect()
2228 for obj in gc.get_objects():
2229 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002230 self.log.debug("Leaked git repo object: 0x%x %s" %
2231 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002232 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002233 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002234 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002235 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002236 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002237 for tenant in self.sched.abide.tenants.values():
2238 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002239 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002240 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002241
2242 def shutdown(self):
2243 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002244 self.executor_server.hold_jobs_in_build = False
2245 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002246 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002247 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002248 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002249 self.sched.stop()
2250 self.sched.join()
2251 self.statsd.stop()
2252 self.statsd.join()
2253 self.webapp.stop()
2254 self.webapp.join()
2255 self.rpc.stop()
2256 self.rpc.join()
2257 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002258 self.fake_nodepool.stop()
2259 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002260 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002261 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002262 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002263 # Further the pydevd threads also need to be whitelisted so debugging
2264 # e.g. in PyCharm is possible without breaking shutdown.
2265 whitelist = ['executor-watchdog',
2266 'pydevd.CommandThread',
2267 'pydevd.Reader',
2268 'pydevd.Writer',
2269 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002270 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002271 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002272 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002273 log_str = ""
2274 for thread_id, stack_frame in sys._current_frames().items():
2275 log_str += "Thread: %s\n" % thread_id
2276 log_str += "".join(traceback.format_stack(stack_frame))
2277 self.log.debug(log_str)
2278 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002279
James E. Blaira002b032017-04-18 10:35:48 -07002280 def assertCleanShutdown(self):
2281 pass
2282
James E. Blairc4ba97a2017-04-19 16:26:24 -07002283 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002284 parts = project.split('/')
2285 path = os.path.join(self.upstream_root, *parts[:-1])
2286 if not os.path.exists(path):
2287 os.makedirs(path)
2288 path = os.path.join(self.upstream_root, project)
2289 repo = git.Repo.init(path)
2290
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002291 with repo.config_writer() as config_writer:
2292 config_writer.set_value('user', 'email', 'user@example.com')
2293 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002294
Clark Boylanb640e052014-04-03 16:41:46 -07002295 repo.index.commit('initial commit')
2296 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002297 if tag:
2298 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002299
James E. Blair97d902e2014-08-21 13:25:56 -07002300 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002301 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002302 repo.git.clean('-x', '-f', '-d')
2303
James E. Blair97d902e2014-08-21 13:25:56 -07002304 def create_branch(self, project, branch):
2305 path = os.path.join(self.upstream_root, project)
2306 repo = git.Repo.init(path)
2307 fn = os.path.join(path, 'README')
2308
2309 branch_head = repo.create_head(branch)
2310 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002311 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002312 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002313 f.close()
2314 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002315 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002316
James E. Blair97d902e2014-08-21 13:25:56 -07002317 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002318 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002319 repo.git.clean('-x', '-f', '-d')
2320
Sachi King9f16d522016-03-16 12:20:45 +11002321 def create_commit(self, project):
2322 path = os.path.join(self.upstream_root, project)
2323 repo = git.Repo(path)
2324 repo.head.reference = repo.heads['master']
2325 file_name = os.path.join(path, 'README')
2326 with open(file_name, 'a') as f:
2327 f.write('creating fake commit\n')
2328 repo.index.add([file_name])
2329 commit = repo.index.commit('Creating a fake commit')
2330 return commit.hexsha
2331
James E. Blairf4a5f022017-04-18 14:01:10 -07002332 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002333 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002334 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002335 while len(self.builds):
2336 self.release(self.builds[0])
2337 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002338 i += 1
2339 if count is not None and i >= count:
2340 break
James E. Blairb8c16472015-05-05 14:55:26 -07002341
James E. Blairdf25ddc2017-07-08 07:57:09 -07002342 def getSortedBuilds(self):
2343 "Return the list of currently running builds sorted by name"
2344
2345 return sorted(self.builds, key=lambda x: x.name)
2346
Clark Boylanb640e052014-04-03 16:41:46 -07002347 def release(self, job):
2348 if isinstance(job, FakeBuild):
2349 job.release()
2350 else:
2351 job.waiting = False
2352 self.log.debug("Queued job %s released" % job.unique)
2353 self.gearman_server.wakeConnections()
2354
2355 def getParameter(self, job, name):
2356 if isinstance(job, FakeBuild):
2357 return job.parameters[name]
2358 else:
2359 parameters = json.loads(job.arguments)
2360 return parameters[name]
2361
Clark Boylanb640e052014-04-03 16:41:46 -07002362 def haveAllBuildsReported(self):
2363 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002364 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002365 return False
2366 # Find out if every build that the worker has completed has been
2367 # reported back to Zuul. If it hasn't then that means a Gearman
2368 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002369 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002370 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002371 if not zbuild:
2372 # It has already been reported
2373 continue
2374 # It hasn't been reported yet.
2375 return False
2376 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002377 worker = self.executor_server.executor_worker
2378 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002379 if connection.state == 'GRAB_WAIT':
2380 return False
2381 return True
2382
2383 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002384 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002385 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002386 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002387 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002388 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002389 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002390 for j in conn.related_jobs.values():
2391 if j.unique == build.uuid:
2392 client_job = j
2393 break
2394 if not client_job:
2395 self.log.debug("%s is not known to the gearman client" %
2396 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002397 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002398 if not client_job.handle:
2399 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002400 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002401 server_job = self.gearman_server.jobs.get(client_job.handle)
2402 if not server_job:
2403 self.log.debug("%s is not known to the gearman server" %
2404 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002405 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002406 if not hasattr(server_job, 'waiting'):
2407 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002408 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002409 if server_job.waiting:
2410 continue
James E. Blair17302972016-08-10 16:11:42 -07002411 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002412 self.log.debug("%s has not reported start" % build)
2413 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002414 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002415 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002416 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002417 if worker_build:
2418 if worker_build.isWaiting():
2419 continue
2420 else:
2421 self.log.debug("%s is running" % worker_build)
2422 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002423 else:
James E. Blair962220f2016-08-03 11:22:38 -07002424 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002425 return False
James E. Blaira002b032017-04-18 10:35:48 -07002426 for (build_uuid, job_worker) in \
2427 self.executor_server.job_workers.items():
2428 if build_uuid not in seen_builds:
2429 self.log.debug("%s is not finalized" % build_uuid)
2430 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002431 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002432
James E. Blairdce6cea2016-12-20 16:45:32 -08002433 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002434 if self.fake_nodepool.paused:
2435 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002436 if self.sched.nodepool.requests:
2437 return False
2438 return True
2439
Jan Hruban6b71aff2015-10-22 16:58:08 +02002440 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002441 for event_queue in self.event_queues:
2442 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002443
2444 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002445 for event_queue in self.event_queues:
2446 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002447
Clark Boylanb640e052014-04-03 16:41:46 -07002448 def waitUntilSettled(self):
2449 self.log.debug("Waiting until settled...")
2450 start = time.time()
2451 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002452 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002453 self.log.error("Timeout waiting for Zuul to settle")
2454 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002455 for event_queue in self.event_queues:
2456 self.log.error(" %s: %s" %
2457 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002458 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002459 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002460 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002461 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002462 self.log.error("All requests completed: %s" %
2463 (self.areAllNodeRequestsComplete(),))
2464 self.log.error("Merge client jobs: %s" %
2465 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002466 raise Exception("Timeout waiting for Zuul to settle")
2467 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002468
Paul Belanger174a8272017-03-14 13:20:10 -04002469 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002470 # have all build states propogated to zuul?
2471 if self.haveAllBuildsReported():
2472 # Join ensures that the queue is empty _and_ events have been
2473 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002474 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002475 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002476 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002477 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002478 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002479 self.areAllNodeRequestsComplete() and
2480 all(self.eventQueuesEmpty())):
2481 # The queue empty check is placed at the end to
2482 # ensure that if a component adds an event between
2483 # when locked the run handler and checked that the
2484 # components were stable, we don't erroneously
2485 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002486 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002487 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002488 self.log.debug("...settled.")
2489 return
2490 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002491 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002492 self.sched.wake_event.wait(0.1)
2493
2494 def countJobResults(self, jobs, result):
2495 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002496 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002497
Monty Taylor0d926122017-05-24 08:07:56 -05002498 def getBuildByName(self, name):
2499 for build in self.builds:
2500 if build.name == name:
2501 return build
2502 raise Exception("Unable to find build %s" % name)
2503
James E. Blair96c6bf82016-01-15 16:20:40 -08002504 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002505 for job in self.history:
2506 if (job.name == name and
2507 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002508 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002509 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002510 raise Exception("Unable to find job %s in history" % name)
2511
2512 def assertEmptyQueues(self):
2513 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002514 for tenant in self.sched.abide.tenants.values():
2515 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002516 for pipeline_queue in pipeline.queues:
2517 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002518 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002519 pipeline.name, pipeline_queue.name,
2520 pipeline_queue.queue))
2521 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002522 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002523
2524 def assertReportedStat(self, key, value=None, kind=None):
2525 start = time.time()
2526 while time.time() < (start + 5):
2527 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002528 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002529 if key == k:
2530 if value is None and kind is None:
2531 return
2532 elif value:
2533 if value == v:
2534 return
2535 elif kind:
2536 if v.endswith('|' + kind):
2537 return
2538 time.sleep(0.1)
2539
Clark Boylanb640e052014-04-03 16:41:46 -07002540 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002541
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002542 def assertBuilds(self, builds):
2543 """Assert that the running builds are as described.
2544
2545 The list of running builds is examined and must match exactly
2546 the list of builds described by the input.
2547
2548 :arg list builds: A list of dictionaries. Each item in the
2549 list must match the corresponding build in the build
2550 history, and each element of the dictionary must match the
2551 corresponding attribute of the build.
2552
2553 """
James E. Blair3158e282016-08-19 09:34:11 -07002554 try:
2555 self.assertEqual(len(self.builds), len(builds))
2556 for i, d in enumerate(builds):
2557 for k, v in d.items():
2558 self.assertEqual(
2559 getattr(self.builds[i], k), v,
2560 "Element %i in builds does not match" % (i,))
2561 except Exception:
2562 for build in self.builds:
2563 self.log.error("Running build: %s" % build)
2564 else:
2565 self.log.error("No running builds")
2566 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002567
James E. Blairb536ecc2016-08-31 10:11:42 -07002568 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002569 """Assert that the completed builds are as described.
2570
2571 The list of completed builds is examined and must match
2572 exactly the list of builds described by the input.
2573
2574 :arg list history: A list of dictionaries. Each item in the
2575 list must match the corresponding build in the build
2576 history, and each element of the dictionary must match the
2577 corresponding attribute of the build.
2578
James E. Blairb536ecc2016-08-31 10:11:42 -07002579 :arg bool ordered: If true, the history must match the order
2580 supplied, if false, the builds are permitted to have
2581 arrived in any order.
2582
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002583 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002584 def matches(history_item, item):
2585 for k, v in item.items():
2586 if getattr(history_item, k) != v:
2587 return False
2588 return True
James E. Blair3158e282016-08-19 09:34:11 -07002589 try:
2590 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002591 if ordered:
2592 for i, d in enumerate(history):
2593 if not matches(self.history[i], d):
2594 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002595 "Element %i in history does not match %s" %
2596 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002597 else:
2598 unseen = self.history[:]
2599 for i, d in enumerate(history):
2600 found = False
2601 for unseen_item in unseen:
2602 if matches(unseen_item, d):
2603 found = True
2604 unseen.remove(unseen_item)
2605 break
2606 if not found:
2607 raise Exception("No match found for element %i "
2608 "in history" % (i,))
2609 if unseen:
2610 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002611 except Exception:
2612 for build in self.history:
2613 self.log.error("Completed build: %s" % build)
2614 else:
2615 self.log.error("No completed builds")
2616 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002617
James E. Blair6ac368c2016-12-22 18:07:20 -08002618 def printHistory(self):
2619 """Log the build history.
2620
2621 This can be useful during tests to summarize what jobs have
2622 completed.
2623
2624 """
2625 self.log.debug("Build history:")
2626 for build in self.history:
2627 self.log.debug(build)
2628
James E. Blair59fdbac2015-12-07 17:08:06 -08002629 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002630 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2631
James E. Blair9ea70072017-04-19 16:05:30 -07002632 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002633 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002634 if not os.path.exists(root):
2635 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002636 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2637 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002638- tenant:
2639 name: openstack
2640 source:
2641 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002642 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002643 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002644 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002645 - org/project
2646 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002647 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002648 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002649 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002650 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002651 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002652
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002653 def addCommitToRepo(self, project, message, files,
2654 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002655 path = os.path.join(self.upstream_root, project)
2656 repo = git.Repo(path)
2657 repo.head.reference = branch
2658 zuul.merger.merger.reset_repo_to_head(repo)
2659 for fn, content in files.items():
2660 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002661 try:
2662 os.makedirs(os.path.dirname(fn))
2663 except OSError:
2664 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002665 with open(fn, 'w') as f:
2666 f.write(content)
2667 repo.index.add([fn])
2668 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002669 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002670 repo.heads[branch].commit = commit
2671 repo.head.reference = branch
2672 repo.git.clean('-x', '-f', '-d')
2673 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002674 if tag:
2675 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002676 return before
2677
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002678 def commitConfigUpdate(self, project_name, source_name):
2679 """Commit an update to zuul.yaml
2680
2681 This overwrites the zuul.yaml in the specificed project with
2682 the contents specified.
2683
2684 :arg str project_name: The name of the project containing
2685 zuul.yaml (e.g., common-config)
2686
2687 :arg str source_name: The path to the file (underneath the
2688 test fixture directory) whose contents should be used to
2689 replace zuul.yaml.
2690 """
2691
2692 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002693 files = {}
2694 with open(source_path, 'r') as f:
2695 data = f.read()
2696 layout = yaml.safe_load(data)
2697 files['zuul.yaml'] = data
2698 for item in layout:
2699 if 'job' in item:
2700 jobname = item['job']['name']
2701 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002702 before = self.addCommitToRepo(
2703 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002704 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002705 return before
2706
James E. Blair7fc8daa2016-08-08 15:37:15 -07002707 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002708
James E. Blair7fc8daa2016-08-08 15:37:15 -07002709 """Inject a Fake (Gerrit) event.
2710
2711 This method accepts a JSON-encoded event and simulates Zuul
2712 having received it from Gerrit. It could (and should)
2713 eventually apply to any connection type, but is currently only
2714 used with Gerrit connections. The name of the connection is
2715 used to look up the corresponding server, and the event is
2716 simulated as having been received by all Zuul connections
2717 attached to that server. So if two Gerrit connections in Zuul
2718 are connected to the same Gerrit server, and you invoke this
2719 method specifying the name of one of them, the event will be
2720 received by both.
2721
2722 .. note::
2723
2724 "self.fake_gerrit.addEvent" calls should be migrated to
2725 this method.
2726
2727 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002728 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002729 :arg str event: The JSON-encoded event.
2730
2731 """
2732 specified_conn = self.connections.connections[connection]
2733 for conn in self.connections.connections.values():
2734 if (isinstance(conn, specified_conn.__class__) and
2735 specified_conn.server == conn.server):
2736 conn.addEvent(event)
2737
James E. Blaird8af5422017-05-24 13:59:40 -07002738 def getUpstreamRepos(self, projects):
2739 """Return upstream git repo objects for the listed projects
2740
2741 :arg list projects: A list of strings, each the canonical name
2742 of a project.
2743
2744 :returns: A dictionary of {name: repo} for every listed
2745 project.
2746 :rtype: dict
2747
2748 """
2749
2750 repos = {}
2751 for project in projects:
2752 # FIXME(jeblair): the upstream root does not yet have a
2753 # hostname component; that needs to be added, and this
2754 # line removed:
2755 tmp_project_name = '/'.join(project.split('/')[1:])
2756 path = os.path.join(self.upstream_root, tmp_project_name)
2757 repo = git.Repo(path)
2758 repos[project] = repo
2759 return repos
2760
James E. Blair3f876d52016-07-22 13:07:14 -07002761
2762class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002763 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002764 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002765
Jamie Lennox7655b552017-03-17 12:33:38 +11002766 @contextmanager
2767 def jobLog(self, build):
2768 """Print job logs on assertion errors
2769
2770 This method is a context manager which, if it encounters an
2771 ecxeption, adds the build log to the debug output.
2772
2773 :arg Build build: The build that's being asserted.
2774 """
2775 try:
2776 yield
2777 except Exception:
2778 path = os.path.join(self.test_root, build.uuid,
2779 'work', 'logs', 'job-output.txt')
2780 with open(path) as f:
2781 self.log.debug(f.read())
2782 raise
2783
Joshua Heskethd78b4482015-09-14 16:56:34 -06002784
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002785class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002786 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002787 use_ssl = True
2788
2789
Joshua Heskethd78b4482015-09-14 16:56:34 -06002790class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002791 def setup_config(self):
2792 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002793 for section_name in self.config.sections():
2794 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2795 section_name, re.I)
2796 if not con_match:
2797 continue
2798
2799 if self.config.get(section_name, 'driver') == 'sql':
2800 f = MySQLSchemaFixture()
2801 self.useFixture(f)
2802 if (self.config.get(section_name, 'dburi') ==
2803 '$MYSQL_FIXTURE_DBURI$'):
2804 self.config.set(section_name, 'dburi', f.dburi)