blob: eca31fe52e94e3b1a200302f57f186c71b712a0e [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
Tobias Henkel64e37a02017-08-02 10:13:30 +0200554class FakeGithub(object):
555
556 class FakeUser(object):
557 def __init__(self, login):
558 self.login = login
559 self.name = "Github User"
560 self.email = "github.user@example.com"
561
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200562 class FakeBranch(object):
563 def __init__(self, branch='master'):
564 self.name = branch
565
566 class FakeRepository(object):
567 def __init__(self):
568 self._branches = [FakeGithub.FakeBranch()]
569
570 def branches(self):
571 return self._branches
572
Tobias Henkel64e37a02017-08-02 10:13:30 +0200573 def user(self, login):
574 return self.FakeUser(login)
575
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200576 def repository(self, owner, proj):
577 repo = self.FakeRepository()
578 return repo
579
Tobias Henkel64e37a02017-08-02 10:13:30 +0200580
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581class FakeGithubPullRequest(object):
582
583 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800584 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700585 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700586 """Creates a new PR with several commits.
587 Sends an event about opened PR."""
588 self.github = github
589 self.source = github
590 self.number = number
591 self.project = project
592 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100593 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700594 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100595 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700596 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100597 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700598 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100599 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100600 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800601 self.reviews = []
602 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700603 self.updated_at = None
604 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100605 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100606 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700607 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700608 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100609 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700610 self._updateTimeStamp()
611
Jan Hruban570d01c2016-03-10 21:51:32 +0100612 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700613 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100614 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700615 self._updateTimeStamp()
616
Jan Hruban570d01c2016-03-10 21:51:32 +0100617 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700618 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100619 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700620 self._updateTimeStamp()
621
622 def getPullRequestOpenedEvent(self):
623 return self._getPullRequestEvent('opened')
624
625 def getPullRequestSynchronizeEvent(self):
626 return self._getPullRequestEvent('synchronize')
627
628 def getPullRequestReopenedEvent(self):
629 return self._getPullRequestEvent('reopened')
630
631 def getPullRequestClosedEvent(self):
632 return self._getPullRequestEvent('closed')
633
Jesse Keatinga41566f2017-06-14 18:17:51 -0700634 def getPullRequestEditedEvent(self):
635 return self._getPullRequestEvent('edited')
636
Gregory Haynes4fc12542015-04-22 20:38:06 -0700637 def addComment(self, message):
638 self.comments.append(message)
639 self._updateTimeStamp()
640
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200641 def getCommentAddedEvent(self, text):
642 name = 'issue_comment'
643 data = {
644 'action': 'created',
645 'issue': {
646 'number': self.number
647 },
648 'comment': {
649 'body': text
650 },
651 'repository': {
652 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100653 },
654 'sender': {
655 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200656 }
657 }
658 return (name, data)
659
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800660 def getReviewAddedEvent(self, review):
661 name = 'pull_request_review'
662 data = {
663 'action': 'submitted',
664 'pull_request': {
665 'number': self.number,
666 'title': self.subject,
667 'updated_at': self.updated_at,
668 'base': {
669 'ref': self.branch,
670 'repo': {
671 'full_name': self.project
672 }
673 },
674 'head': {
675 'sha': self.head_sha
676 }
677 },
678 'review': {
679 'state': review
680 },
681 'repository': {
682 'full_name': self.project
683 },
684 'sender': {
685 'login': 'ghuser'
686 }
687 }
688 return (name, data)
689
Jan Hruban16ad31f2015-11-07 14:39:07 +0100690 def addLabel(self, name):
691 if name not in self.labels:
692 self.labels.append(name)
693 self._updateTimeStamp()
694 return self._getLabelEvent(name)
695
696 def removeLabel(self, name):
697 if name in self.labels:
698 self.labels.remove(name)
699 self._updateTimeStamp()
700 return self._getUnlabelEvent(name)
701
702 def _getLabelEvent(self, label):
703 name = 'pull_request'
704 data = {
705 'action': 'labeled',
706 'pull_request': {
707 'number': self.number,
708 'updated_at': self.updated_at,
709 'base': {
710 'ref': self.branch,
711 'repo': {
712 'full_name': self.project
713 }
714 },
715 'head': {
716 'sha': self.head_sha
717 }
718 },
719 'label': {
720 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100721 },
722 'sender': {
723 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100724 }
725 }
726 return (name, data)
727
728 def _getUnlabelEvent(self, label):
729 name = 'pull_request'
730 data = {
731 'action': 'unlabeled',
732 'pull_request': {
733 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100734 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100735 'updated_at': self.updated_at,
736 'base': {
737 'ref': self.branch,
738 'repo': {
739 'full_name': self.project
740 }
741 },
742 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800743 'sha': self.head_sha,
744 'repo': {
745 'full_name': self.project
746 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100747 }
748 },
749 'label': {
750 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100751 },
752 'sender': {
753 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100754 }
755 }
756 return (name, data)
757
Jesse Keatinga41566f2017-06-14 18:17:51 -0700758 def editBody(self, body):
759 self.body = body
760 self._updateTimeStamp()
761
Gregory Haynes4fc12542015-04-22 20:38:06 -0700762 def _getRepo(self):
763 repo_path = os.path.join(self.upstream_root, self.project)
764 return git.Repo(repo_path)
765
766 def _createPRRef(self):
767 repo = self._getRepo()
768 GithubChangeReference.create(
769 repo, self._getPRReference(), 'refs/tags/init')
770
Jan Hruban570d01c2016-03-10 21:51:32 +0100771 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700772 repo = self._getRepo()
773 ref = repo.references[self._getPRReference()]
774 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100775 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700776 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100777 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700778 repo.head.reference = ref
779 zuul.merger.merger.reset_repo_to_head(repo)
780 repo.git.clean('-x', '-f', '-d')
781
Jan Hruban570d01c2016-03-10 21:51:32 +0100782 if files:
783 fn = files[0]
784 self.files = files
785 else:
786 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
787 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100788 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700789 fn = os.path.join(repo.working_dir, fn)
790 f = open(fn, 'w')
791 with open(fn, 'w') as f:
792 f.write("test %s %s\n" %
793 (self.branch, self.number))
794 repo.index.add([fn])
795
796 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800797 # Create an empty set of statuses for the given sha,
798 # each sha on a PR may have a status set on it
799 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700800 repo.head.reference = 'master'
801 zuul.merger.merger.reset_repo_to_head(repo)
802 repo.git.clean('-x', '-f', '-d')
803 repo.heads['master'].checkout()
804
805 def _updateTimeStamp(self):
806 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
807
808 def getPRHeadSha(self):
809 repo = self._getRepo()
810 return repo.references[self._getPRReference()].commit.hexsha
811
Jesse Keatingae4cd272017-01-30 17:10:44 -0800812 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800813 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
814 # convert the timestamp to a str format that would be returned
815 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800816
Adam Gandelmand81dd762017-02-09 15:15:49 -0800817 if granted_on:
818 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
819 submitted_at = time.strftime(
820 gh_time_format, granted_on.timetuple())
821 else:
822 # github timestamps only down to the second, so we need to make
823 # sure reviews that tests add appear to be added over a period of
824 # time in the past and not all at once.
825 if not self.reviews:
826 # the first review happens 10 mins ago
827 offset = 600
828 else:
829 # subsequent reviews happen 1 minute closer to now
830 offset = 600 - (len(self.reviews) * 60)
831
832 granted_on = datetime.datetime.utcfromtimestamp(
833 time.time() - offset)
834 submitted_at = time.strftime(
835 gh_time_format, granted_on.timetuple())
836
Jesse Keatingae4cd272017-01-30 17:10:44 -0800837 self.reviews.append({
838 'state': state,
839 'user': {
840 'login': user,
841 'email': user + "@derp.com",
842 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800843 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800844 })
845
Gregory Haynes4fc12542015-04-22 20:38:06 -0700846 def _getPRReference(self):
847 return '%s/head' % self.number
848
849 def _getPullRequestEvent(self, action):
850 name = 'pull_request'
851 data = {
852 'action': action,
853 'number': self.number,
854 'pull_request': {
855 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100856 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700857 'updated_at': self.updated_at,
858 'base': {
859 'ref': self.branch,
860 'repo': {
861 'full_name': self.project
862 }
863 },
864 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800865 'sha': self.head_sha,
866 'repo': {
867 'full_name': self.project
868 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700869 },
870 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100871 },
872 'sender': {
873 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700874 }
875 }
876 return (name, data)
877
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800878 def getCommitStatusEvent(self, context, state='success', user='zuul'):
879 name = 'status'
880 data = {
881 'state': state,
882 'sha': self.head_sha,
883 'description': 'Test results for %s: %s' % (self.head_sha, state),
884 'target_url': 'http://zuul/%s' % self.head_sha,
885 'branches': [],
886 'context': context,
887 'sender': {
888 'login': user
889 }
890 }
891 return (name, data)
892
Gregory Haynes4fc12542015-04-22 20:38:06 -0700893
894class FakeGithubConnection(githubconnection.GithubConnection):
895 log = logging.getLogger("zuul.test.FakeGithubConnection")
896
897 def __init__(self, driver, connection_name, connection_config,
898 upstream_root=None):
899 super(FakeGithubConnection, self).__init__(driver, connection_name,
900 connection_config)
901 self.connection_name = connection_name
902 self.pr_number = 0
903 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700904 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700905 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100906 self.merge_failure = False
907 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100908 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +0200909 self.github_client = FakeGithub()
910
911 def getGithubClient(self,
912 project=None,
913 user_id=None,
914 use_app=True):
915 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700916
Jesse Keatinga41566f2017-06-14 18:17:51 -0700917 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700918 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700919 self.pr_number += 1
920 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100921 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700922 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700923 self.pull_requests.append(pull_request)
924 return pull_request
925
Jesse Keating71a47ff2017-06-06 11:36:43 -0700926 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
927 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700928 if not old_rev:
929 old_rev = '00000000000000000000000000000000'
930 if not new_rev:
931 new_rev = random_sha1()
932 name = 'push'
933 data = {
934 'ref': ref,
935 'before': old_rev,
936 'after': new_rev,
937 'repository': {
938 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700939 },
940 'commits': [
941 {
942 'added': added_files,
943 'removed': removed_files,
944 'modified': modified_files
945 }
946 ]
Wayne1a78c612015-06-11 17:14:13 -0700947 }
948 return (name, data)
949
Gregory Haynes4fc12542015-04-22 20:38:06 -0700950 def emitEvent(self, event):
951 """Emulates sending the GitHub webhook event to the connection."""
952 port = self.webapp.server.socket.getsockname()[1]
953 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700954 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -0700955 secret = self.connection_config['webhook_token']
956 signature = githubconnection._sign_request(payload, secret)
957 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700958 req = urllib.request.Request(
959 'http://localhost:%s/connection/%s/payload'
960 % (port, self.connection_name),
961 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000962 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700963
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200964 def getPull(self, project, number):
965 pr = self.pull_requests[number - 1]
966 data = {
967 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100968 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200969 'updated_at': pr.updated_at,
970 'base': {
971 'repo': {
972 'full_name': pr.project
973 },
974 'ref': pr.branch,
975 },
Jan Hruban37615e52015-11-19 14:30:49 +0100976 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700977 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200978 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800979 'sha': pr.head_sha,
980 'repo': {
981 'full_name': pr.project
982 }
Jesse Keating61040e72017-06-08 15:08:27 -0700983 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700984 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700985 'labels': pr.labels,
986 'merged': pr.is_merged,
987 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200988 }
989 return data
990
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800991 def getPullBySha(self, sha):
992 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
993 if len(prs) > 1:
994 raise Exception('Multiple pulls found with head sha: %s' % sha)
995 pr = prs[0]
996 return self.getPull(pr.project, pr.number)
997
Jesse Keatingae4cd272017-01-30 17:10:44 -0800998 def _getPullReviews(self, owner, project, number):
999 pr = self.pull_requests[number - 1]
1000 return pr.reviews
1001
Jesse Keatingae4cd272017-01-30 17:10:44 -08001002 def getRepoPermission(self, project, login):
1003 owner, proj = project.split('/')
1004 for pr in self.pull_requests:
1005 pr_owner, pr_project = pr.project.split('/')
1006 if (pr_owner == owner and proj == pr_project):
1007 if login in pr.writers:
1008 return 'write'
1009 else:
1010 return 'read'
1011
Gregory Haynes4fc12542015-04-22 20:38:06 -07001012 def getGitUrl(self, project):
1013 return os.path.join(self.upstream_root, str(project))
1014
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001015 def real_getGitUrl(self, project):
1016 return super(FakeGithubConnection, self).getGitUrl(project)
1017
Jan Hrubane252a732017-01-03 15:03:09 +01001018 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001019 # record that this got reported
1020 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001021 pull_request = self.pull_requests[pr_number - 1]
1022 pull_request.addComment(message)
1023
Jan Hruban3b415922016-02-03 13:10:22 +01001024 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001025 # record that this got reported
1026 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001027 pull_request = self.pull_requests[pr_number - 1]
1028 if self.merge_failure:
1029 raise Exception('Pull request was not merged')
1030 if self.merge_not_allowed_count > 0:
1031 self.merge_not_allowed_count -= 1
1032 raise MergeFailure('Merge was not successful due to mergeability'
1033 ' conflict')
1034 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001035 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001036
Jesse Keatingd96e5882017-01-19 13:55:50 -08001037 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001038 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001039
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001040 def setCommitStatus(self, project, sha, state, url='', description='',
1041 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001042 # record that this got reported
1043 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001044 # always insert a status to the front of the list, to represent
1045 # the last status provided for a commit.
1046 # Since we're bypassing github API, which would require a user, we
1047 # default the user as 'zuul' here.
1048 self.statuses.setdefault(project, {}).setdefault(sha, [])
1049 self.statuses[project][sha].insert(0, {
1050 'state': state,
1051 'url': url,
1052 'description': description,
1053 'context': context,
1054 'creator': {
1055 'login': user
1056 }
1057 })
Jan Hrubane252a732017-01-03 15:03:09 +01001058
Jan Hruban16ad31f2015-11-07 14:39:07 +01001059 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001060 # record that this got reported
1061 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001062 pull_request = self.pull_requests[pr_number - 1]
1063 pull_request.addLabel(label)
1064
1065 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001066 # record that this got reported
1067 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001068 pull_request = self.pull_requests[pr_number - 1]
1069 pull_request.removeLabel(label)
1070
Jesse Keatinga41566f2017-06-14 18:17:51 -07001071 def _getNeededByFromPR(self, change):
1072 prs = []
1073 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001074 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001075 change.number))
1076 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001077 if not pr.body:
1078 body = ''
1079 else:
1080 body = pr.body
1081 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001082 # Get our version of a pull so that it's a dict
1083 pull = self.getPull(pr.project, pr.number)
1084 prs.append(pull)
1085
1086 return prs
1087
Gregory Haynes4fc12542015-04-22 20:38:06 -07001088
Clark Boylanb640e052014-04-03 16:41:46 -07001089class BuildHistory(object):
1090 def __init__(self, **kw):
1091 self.__dict__.update(kw)
1092
1093 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001094 return ("<Completed build, result: %s name: %s uuid: %s "
1095 "changes: %s ref: %s>" %
1096 (self.result, self.name, self.uuid,
1097 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001098
1099
Clark Boylanb640e052014-04-03 16:41:46 -07001100class FakeStatsd(threading.Thread):
1101 def __init__(self):
1102 threading.Thread.__init__(self)
1103 self.daemon = True
1104 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1105 self.sock.bind(('', 0))
1106 self.port = self.sock.getsockname()[1]
1107 self.wake_read, self.wake_write = os.pipe()
1108 self.stats = []
1109
1110 def run(self):
1111 while True:
1112 poll = select.poll()
1113 poll.register(self.sock, select.POLLIN)
1114 poll.register(self.wake_read, select.POLLIN)
1115 ret = poll.poll()
1116 for (fd, event) in ret:
1117 if fd == self.sock.fileno():
1118 data = self.sock.recvfrom(1024)
1119 if not data:
1120 return
1121 self.stats.append(data[0])
1122 if fd == self.wake_read:
1123 return
1124
1125 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001126 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001127
1128
James E. Blaire1767bc2016-08-02 10:00:27 -07001129class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001130 log = logging.getLogger("zuul.test")
1131
Paul Belanger174a8272017-03-14 13:20:10 -04001132 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001133 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001134 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001135 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001136 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001137 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001139 # TODOv3(jeblair): self.node is really "the label of the node
1140 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001141 # keep using it like this, or we may end up exposing more of
1142 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001143 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001144 self.node = None
1145 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001146 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001147 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001148 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001149 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001150 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001151 self.wait_condition = threading.Condition()
1152 self.waiting = False
1153 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001154 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001155 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001156 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001157 items = self.parameters['zuul']['items']
1158 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1159 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001160
James E. Blair3158e282016-08-19 09:34:11 -07001161 def __repr__(self):
1162 waiting = ''
1163 if self.waiting:
1164 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001165 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1166 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001167
Clark Boylanb640e052014-04-03 16:41:46 -07001168 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001169 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001170 self.wait_condition.acquire()
1171 self.wait_condition.notify()
1172 self.waiting = False
1173 self.log.debug("Build %s released" % self.unique)
1174 self.wait_condition.release()
1175
1176 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001177 """Return whether this build is being held.
1178
1179 :returns: Whether the build is being held.
1180 :rtype: bool
1181 """
1182
Clark Boylanb640e052014-04-03 16:41:46 -07001183 self.wait_condition.acquire()
1184 if self.waiting:
1185 ret = True
1186 else:
1187 ret = False
1188 self.wait_condition.release()
1189 return ret
1190
1191 def _wait(self):
1192 self.wait_condition.acquire()
1193 self.waiting = True
1194 self.log.debug("Build %s waiting" % self.unique)
1195 self.wait_condition.wait()
1196 self.wait_condition.release()
1197
1198 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001199 self.log.debug('Running build %s' % self.unique)
1200
Paul Belanger174a8272017-03-14 13:20:10 -04001201 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001202 self.log.debug('Holding build %s' % self.unique)
1203 self._wait()
1204 self.log.debug("Build %s continuing" % self.unique)
1205
James E. Blair412fba82017-01-26 15:00:50 -08001206 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001207 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001208 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001209 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001210 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001211 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001212 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001213
James E. Blaire1767bc2016-08-02 10:00:27 -07001214 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001215
James E. Blaira5dba232016-08-08 15:53:24 -07001216 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001217 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001218 for change in changes:
1219 if self.hasChanges(change):
1220 return True
1221 return False
1222
James E. Blaire7b99a02016-08-05 14:27:34 -07001223 def hasChanges(self, *changes):
1224 """Return whether this build has certain changes in its git repos.
1225
1226 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001227 are expected to be present (in order) in the git repository of
1228 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001229
1230 :returns: Whether the build has the indicated changes.
1231 :rtype: bool
1232
1233 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001234 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001235 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001236 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001237 try:
1238 repo = git.Repo(path)
1239 except NoSuchPathError as e:
1240 self.log.debug('%s' % e)
1241 return False
James E. Blair247cab72017-07-20 16:52:36 -07001242 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001243 commit_message = '%s-1' % change.subject
1244 self.log.debug("Checking if build %s has changes; commit_message "
1245 "%s; repo_messages %s" % (self, commit_message,
1246 repo_messages))
1247 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001248 self.log.debug(" messages do not match")
1249 return False
1250 self.log.debug(" OK")
1251 return True
1252
James E. Blaird8af5422017-05-24 13:59:40 -07001253 def getWorkspaceRepos(self, projects):
1254 """Return workspace git repo objects for the listed projects
1255
1256 :arg list projects: A list of strings, each the canonical name
1257 of a project.
1258
1259 :returns: A dictionary of {name: repo} for every listed
1260 project.
1261 :rtype: dict
1262
1263 """
1264
1265 repos = {}
1266 for project in projects:
1267 path = os.path.join(self.jobdir.src_root, project)
1268 repo = git.Repo(path)
1269 repos[project] = repo
1270 return repos
1271
Clark Boylanb640e052014-04-03 16:41:46 -07001272
Paul Belanger174a8272017-03-14 13:20:10 -04001273class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1274 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001275
Paul Belanger174a8272017-03-14 13:20:10 -04001276 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001277 they will report that they have started but then pause until
1278 released before reporting completion. This attribute may be
1279 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001280 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001281 be explicitly released.
1282
1283 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001284 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001285 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001286 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001287 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 self.hold_jobs_in_build = False
1289 self.lock = threading.Lock()
1290 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001291 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001292 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001293 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001294
James E. Blaira5dba232016-08-08 15:53:24 -07001295 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001296 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001297
1298 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001299 :arg Change change: The :py:class:`~tests.base.FakeChange`
1300 instance which should cause the job to fail. This job
1301 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001302
1303 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001304 l = self.fail_tests.get(name, [])
1305 l.append(change)
1306 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001307
James E. Blair962220f2016-08-03 11:22:38 -07001308 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001309 """Release a held build.
1310
1311 :arg str regex: A regular expression which, if supplied, will
1312 cause only builds with matching names to be released. If
1313 not supplied, all builds will be released.
1314
1315 """
James E. Blair962220f2016-08-03 11:22:38 -07001316 builds = self.running_builds[:]
1317 self.log.debug("Releasing build %s (%s)" % (regex,
1318 len(self.running_builds)))
1319 for build in builds:
1320 if not regex or re.match(regex, build.name):
1321 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001322 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001323 build.release()
1324 else:
1325 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001326 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001327 self.log.debug("Done releasing builds %s (%s)" %
1328 (regex, len(self.running_builds)))
1329
Paul Belanger174a8272017-03-14 13:20:10 -04001330 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001331 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001332 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001333 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001334 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001335 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001336 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001337 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001338 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1339 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001340
1341 def stopJob(self, job):
1342 self.log.debug("handle stop")
1343 parameters = json.loads(job.arguments)
1344 uuid = parameters['uuid']
1345 for build in self.running_builds:
1346 if build.unique == uuid:
1347 build.aborted = True
1348 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001349 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001350
James E. Blaira002b032017-04-18 10:35:48 -07001351 def stop(self):
1352 for build in self.running_builds:
1353 build.release()
1354 super(RecordingExecutorServer, self).stop()
1355
Joshua Hesketh50c21782016-10-13 21:34:14 +11001356
Paul Belanger174a8272017-03-14 13:20:10 -04001357class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001358 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001359 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001360 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001361 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001362 if not commit: # merge conflict
1363 self.recordResult('MERGER_FAILURE')
1364 return commit
1365
1366 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001367 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001368 self.executor_server.lock.acquire()
1369 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001370 BuildHistory(name=build.name, result=result, changes=build.changes,
1371 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001372 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001373 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001374 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001375 )
Paul Belanger174a8272017-03-14 13:20:10 -04001376 self.executor_server.running_builds.remove(build)
1377 del self.executor_server.job_builds[self.job.unique]
1378 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001379
1380 def runPlaybooks(self, args):
1381 build = self.executor_server.job_builds[self.job.unique]
1382 build.jobdir = self.jobdir
1383
1384 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1385 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001386 return result
1387
James E. Blair74a82cf2017-07-12 17:23:08 -07001388 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001389 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001390
Paul Belanger174a8272017-03-14 13:20:10 -04001391 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001392 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001393 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001394 else:
1395 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001396 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001397
James E. Blairad8dca02017-02-21 11:48:32 -05001398 def getHostList(self, args):
1399 self.log.debug("hostlist")
1400 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001401 for host in hosts:
1402 host['host_vars']['ansible_connection'] = 'local'
1403
1404 hosts.append(dict(
1405 name='localhost',
1406 host_vars=dict(ansible_connection='local'),
1407 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001408 return hosts
1409
James E. Blairf5dbd002015-12-23 15:26:17 -08001410
Clark Boylanb640e052014-04-03 16:41:46 -07001411class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001412 """A Gearman server for use in tests.
1413
1414 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1415 added to the queue but will not be distributed to workers
1416 until released. This attribute may be changed at any time and
1417 will take effect for subsequently enqueued jobs, but
1418 previously held jobs will still need to be explicitly
1419 released.
1420
1421 """
1422
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001423 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001424 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001425 if use_ssl:
1426 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1427 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1428 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1429 else:
1430 ssl_ca = None
1431 ssl_cert = None
1432 ssl_key = None
1433
1434 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1435 ssl_cert=ssl_cert,
1436 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001437
1438 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001439 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1440 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001441 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001442 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001443 job.waiting = self.hold_jobs_in_queue
1444 else:
1445 job.waiting = False
1446 if job.waiting:
1447 continue
1448 if job.name in connection.functions:
1449 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001450 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001451 connection.related_jobs[job.handle] = job
1452 job.worker_connection = connection
1453 job.running = True
1454 return job
1455 return None
1456
1457 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001458 """Release a held job.
1459
1460 :arg str regex: A regular expression which, if supplied, will
1461 cause only jobs with matching names to be released. If
1462 not supplied, all jobs will be released.
1463 """
Clark Boylanb640e052014-04-03 16:41:46 -07001464 released = False
1465 qlen = (len(self.high_queue) + len(self.normal_queue) +
1466 len(self.low_queue))
1467 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1468 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001469 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001470 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001471 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001472 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001473 self.log.debug("releasing queued job %s" %
1474 job.unique)
1475 job.waiting = False
1476 released = True
1477 else:
1478 self.log.debug("not releasing queued job %s" %
1479 job.unique)
1480 if released:
1481 self.wakeConnections()
1482 qlen = (len(self.high_queue) + len(self.normal_queue) +
1483 len(self.low_queue))
1484 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1485
1486
1487class FakeSMTP(object):
1488 log = logging.getLogger('zuul.FakeSMTP')
1489
1490 def __init__(self, messages, server, port):
1491 self.server = server
1492 self.port = port
1493 self.messages = messages
1494
1495 def sendmail(self, from_email, to_email, msg):
1496 self.log.info("Sending email from %s, to %s, with msg %s" % (
1497 from_email, to_email, msg))
1498
1499 headers = msg.split('\n\n', 1)[0]
1500 body = msg.split('\n\n', 1)[1]
1501
1502 self.messages.append(dict(
1503 from_email=from_email,
1504 to_email=to_email,
1505 msg=msg,
1506 headers=headers,
1507 body=body,
1508 ))
1509
1510 return True
1511
1512 def quit(self):
1513 return True
1514
1515
James E. Blairdce6cea2016-12-20 16:45:32 -08001516class FakeNodepool(object):
1517 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001518 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001519
1520 log = logging.getLogger("zuul.test.FakeNodepool")
1521
1522 def __init__(self, host, port, chroot):
1523 self.client = kazoo.client.KazooClient(
1524 hosts='%s:%s%s' % (host, port, chroot))
1525 self.client.start()
1526 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001527 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001528 self.thread = threading.Thread(target=self.run)
1529 self.thread.daemon = True
1530 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001531 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001532
1533 def stop(self):
1534 self._running = False
1535 self.thread.join()
1536 self.client.stop()
1537 self.client.close()
1538
1539 def run(self):
1540 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001541 try:
1542 self._run()
1543 except Exception:
1544 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001545 time.sleep(0.1)
1546
1547 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001548 if self.paused:
1549 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001550 for req in self.getNodeRequests():
1551 self.fulfillRequest(req)
1552
1553 def getNodeRequests(self):
1554 try:
1555 reqids = self.client.get_children(self.REQUEST_ROOT)
1556 except kazoo.exceptions.NoNodeError:
1557 return []
1558 reqs = []
1559 for oid in sorted(reqids):
1560 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001561 try:
1562 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001563 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001564 data['_oid'] = oid
1565 reqs.append(data)
1566 except kazoo.exceptions.NoNodeError:
1567 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001568 return reqs
1569
James E. Blaire18d4602017-01-05 11:17:28 -08001570 def getNodes(self):
1571 try:
1572 nodeids = self.client.get_children(self.NODE_ROOT)
1573 except kazoo.exceptions.NoNodeError:
1574 return []
1575 nodes = []
1576 for oid in sorted(nodeids):
1577 path = self.NODE_ROOT + '/' + oid
1578 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001579 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001580 data['_oid'] = oid
1581 try:
1582 lockfiles = self.client.get_children(path + '/lock')
1583 except kazoo.exceptions.NoNodeError:
1584 lockfiles = []
1585 if lockfiles:
1586 data['_lock'] = True
1587 else:
1588 data['_lock'] = False
1589 nodes.append(data)
1590 return nodes
1591
James E. Blaira38c28e2017-01-04 10:33:20 -08001592 def makeNode(self, request_id, node_type):
1593 now = time.time()
1594 path = '/nodepool/nodes/'
1595 data = dict(type=node_type,
1596 provider='test-provider',
1597 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001598 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001599 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001600 public_ipv4='127.0.0.1',
1601 private_ipv4=None,
1602 public_ipv6=None,
1603 allocated_to=request_id,
1604 state='ready',
1605 state_time=now,
1606 created_time=now,
1607 updated_time=now,
1608 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001609 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001610 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001611 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001612 path = self.client.create(path, data,
1613 makepath=True,
1614 sequence=True)
1615 nodeid = path.split("/")[-1]
1616 return nodeid
1617
James E. Blair6ab79e02017-01-06 10:10:17 -08001618 def addFailRequest(self, request):
1619 self.fail_requests.add(request['_oid'])
1620
James E. Blairdce6cea2016-12-20 16:45:32 -08001621 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001622 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001623 return
1624 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001625 oid = request['_oid']
1626 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001627
James E. Blair6ab79e02017-01-06 10:10:17 -08001628 if oid in self.fail_requests:
1629 request['state'] = 'failed'
1630 else:
1631 request['state'] = 'fulfilled'
1632 nodes = []
1633 for node in request['node_types']:
1634 nodeid = self.makeNode(oid, node)
1635 nodes.append(nodeid)
1636 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001637
James E. Blaira38c28e2017-01-04 10:33:20 -08001638 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001639 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001640 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001641 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001642 try:
1643 self.client.set(path, data)
1644 except kazoo.exceptions.NoNodeError:
1645 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001646
1647
James E. Blair498059b2016-12-20 13:50:13 -08001648class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001649 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001650 super(ChrootedKazooFixture, self).__init__()
1651
1652 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1653 if ':' in zk_host:
1654 host, port = zk_host.split(':')
1655 else:
1656 host = zk_host
1657 port = None
1658
1659 self.zookeeper_host = host
1660
1661 if not port:
1662 self.zookeeper_port = 2181
1663 else:
1664 self.zookeeper_port = int(port)
1665
Clark Boylan621ec9a2017-04-07 17:41:33 -07001666 self.test_id = test_id
1667
James E. Blair498059b2016-12-20 13:50:13 -08001668 def _setUp(self):
1669 # Make sure the test chroot paths do not conflict
1670 random_bits = ''.join(random.choice(string.ascii_lowercase +
1671 string.ascii_uppercase)
1672 for x in range(8))
1673
Clark Boylan621ec9a2017-04-07 17:41:33 -07001674 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001675 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1676
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001677 self.addCleanup(self._cleanup)
1678
James E. Blair498059b2016-12-20 13:50:13 -08001679 # Ensure the chroot path exists and clean up any pre-existing znodes.
1680 _tmp_client = kazoo.client.KazooClient(
1681 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1682 _tmp_client.start()
1683
1684 if _tmp_client.exists(self.zookeeper_chroot):
1685 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1686
1687 _tmp_client.ensure_path(self.zookeeper_chroot)
1688 _tmp_client.stop()
1689 _tmp_client.close()
1690
James E. Blair498059b2016-12-20 13:50:13 -08001691 def _cleanup(self):
1692 '''Remove the chroot path.'''
1693 # Need a non-chroot'ed client to remove the chroot path
1694 _tmp_client = kazoo.client.KazooClient(
1695 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1696 _tmp_client.start()
1697 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1698 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001699 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001700
1701
Joshua Heskethd78b4482015-09-14 16:56:34 -06001702class MySQLSchemaFixture(fixtures.Fixture):
1703 def setUp(self):
1704 super(MySQLSchemaFixture, self).setUp()
1705
1706 random_bits = ''.join(random.choice(string.ascii_lowercase +
1707 string.ascii_uppercase)
1708 for x in range(8))
1709 self.name = '%s_%s' % (random_bits, os.getpid())
1710 self.passwd = uuid.uuid4().hex
1711 db = pymysql.connect(host="localhost",
1712 user="openstack_citest",
1713 passwd="openstack_citest",
1714 db="openstack_citest")
1715 cur = db.cursor()
1716 cur.execute("create database %s" % self.name)
1717 cur.execute(
1718 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1719 (self.name, self.name, self.passwd))
1720 cur.execute("flush privileges")
1721
1722 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1723 self.passwd,
1724 self.name)
1725 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1726 self.addCleanup(self.cleanup)
1727
1728 def cleanup(self):
1729 db = pymysql.connect(host="localhost",
1730 user="openstack_citest",
1731 passwd="openstack_citest",
1732 db="openstack_citest")
1733 cur = db.cursor()
1734 cur.execute("drop database %s" % self.name)
1735 cur.execute("drop user '%s'@'localhost'" % self.name)
1736 cur.execute("flush privileges")
1737
1738
Maru Newby3fe5f852015-01-13 04:22:14 +00001739class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001740 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001741 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001742
James E. Blair1c236df2017-02-01 14:07:24 -08001743 def attachLogs(self, *args):
1744 def reader():
1745 self._log_stream.seek(0)
1746 while True:
1747 x = self._log_stream.read(4096)
1748 if not x:
1749 break
1750 yield x.encode('utf8')
1751 content = testtools.content.content_from_reader(
1752 reader,
1753 testtools.content_type.UTF8_TEXT,
1754 False)
1755 self.addDetail('logging', content)
1756
Clark Boylanb640e052014-04-03 16:41:46 -07001757 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001758 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001759 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1760 try:
1761 test_timeout = int(test_timeout)
1762 except ValueError:
1763 # If timeout value is invalid do not set a timeout.
1764 test_timeout = 0
1765 if test_timeout > 0:
1766 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1767
1768 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1769 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1770 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1771 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1772 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1773 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1774 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1775 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1776 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1777 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001778 self._log_stream = StringIO()
1779 self.addOnException(self.attachLogs)
1780 else:
1781 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001782
James E. Blair73b41772017-05-22 13:22:55 -07001783 # NOTE(jeblair): this is temporary extra debugging to try to
1784 # track down a possible leak.
1785 orig_git_repo_init = git.Repo.__init__
1786
1787 def git_repo_init(myself, *args, **kw):
1788 orig_git_repo_init(myself, *args, **kw)
1789 self.log.debug("Created git repo 0x%x %s" %
1790 (id(myself), repr(myself)))
1791
1792 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1793 git_repo_init))
1794
James E. Blair1c236df2017-02-01 14:07:24 -08001795 handler = logging.StreamHandler(self._log_stream)
1796 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1797 '%(levelname)-8s %(message)s')
1798 handler.setFormatter(formatter)
1799
1800 logger = logging.getLogger()
1801 logger.setLevel(logging.DEBUG)
1802 logger.addHandler(handler)
1803
Clark Boylan3410d532017-04-25 12:35:29 -07001804 # Make sure we don't carry old handlers around in process state
1805 # which slows down test runs
1806 self.addCleanup(logger.removeHandler, handler)
1807 self.addCleanup(handler.close)
1808 self.addCleanup(handler.flush)
1809
James E. Blair1c236df2017-02-01 14:07:24 -08001810 # NOTE(notmorgan): Extract logging overrides for specific
1811 # libraries from the OS_LOG_DEFAULTS env and create loggers
1812 # for each. This is used to limit the output during test runs
1813 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001814 log_defaults_from_env = os.environ.get(
1815 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001816 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001817
James E. Blairdce6cea2016-12-20 16:45:32 -08001818 if log_defaults_from_env:
1819 for default in log_defaults_from_env.split(','):
1820 try:
1821 name, level_str = default.split('=', 1)
1822 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001823 logger = logging.getLogger(name)
1824 logger.setLevel(level)
1825 logger.addHandler(handler)
1826 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001827 except ValueError:
1828 # NOTE(notmorgan): Invalid format of the log default,
1829 # skip and don't try and apply a logger for the
1830 # specified module
1831 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001832
Maru Newby3fe5f852015-01-13 04:22:14 +00001833
1834class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001835 """A test case with a functioning Zuul.
1836
1837 The following class variables are used during test setup and can
1838 be overidden by subclasses but are effectively read-only once a
1839 test method starts running:
1840
1841 :cvar str config_file: This points to the main zuul config file
1842 within the fixtures directory. Subclasses may override this
1843 to obtain a different behavior.
1844
1845 :cvar str tenant_config_file: This is the tenant config file
1846 (which specifies from what git repos the configuration should
1847 be loaded). It defaults to the value specified in
1848 `config_file` but can be overidden by subclasses to obtain a
1849 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001850 configuration. See also the :py:func:`simple_layout`
1851 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001852
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001853 :cvar bool create_project_keys: Indicates whether Zuul should
1854 auto-generate keys for each project, or whether the test
1855 infrastructure should insert dummy keys to save time during
1856 startup. Defaults to False.
1857
James E. Blaire7b99a02016-08-05 14:27:34 -07001858 The following are instance variables that are useful within test
1859 methods:
1860
1861 :ivar FakeGerritConnection fake_<connection>:
1862 A :py:class:`~tests.base.FakeGerritConnection` will be
1863 instantiated for each connection present in the config file
1864 and stored here. For instance, `fake_gerrit` will hold the
1865 FakeGerritConnection object for a connection named `gerrit`.
1866
1867 :ivar FakeGearmanServer gearman_server: An instance of
1868 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1869 server that all of the Zuul components in this test use to
1870 communicate with each other.
1871
Paul Belanger174a8272017-03-14 13:20:10 -04001872 :ivar RecordingExecutorServer executor_server: An instance of
1873 :py:class:`~tests.base.RecordingExecutorServer` which is the
1874 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001875
1876 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1877 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001878 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001879 list upon completion.
1880
1881 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1882 objects representing completed builds. They are appended to
1883 the list in the order they complete.
1884
1885 """
1886
James E. Blair83005782015-12-11 14:46:03 -08001887 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001888 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001889 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001890 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001891
1892 def _startMerger(self):
1893 self.merge_server = zuul.merger.server.MergeServer(self.config,
1894 self.connections)
1895 self.merge_server.start()
1896
Maru Newby3fe5f852015-01-13 04:22:14 +00001897 def setUp(self):
1898 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001899
1900 self.setupZK()
1901
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001902 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001903 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001904 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1905 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001906 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001907 tmp_root = tempfile.mkdtemp(
1908 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001909 self.test_root = os.path.join(tmp_root, "zuul-test")
1910 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001911 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001912 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001913 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001914 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1915 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001916
1917 if os.path.exists(self.test_root):
1918 shutil.rmtree(self.test_root)
1919 os.makedirs(self.test_root)
1920 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001921 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001922 os.makedirs(self.merger_state_root)
1923 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001924
1925 # Make per test copy of Configuration.
1926 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001927 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1928 if not os.path.exists(self.private_key_file):
1929 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1930 shutil.copy(src_private_key_file, self.private_key_file)
1931 shutil.copy('{}.pub'.format(src_private_key_file),
1932 '{}.pub'.format(self.private_key_file))
1933 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001934 self.config.set('scheduler', 'tenant_config',
1935 os.path.join(
1936 FIXTURE_DIR,
1937 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001938 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001939 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001940 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001941 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001942 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001943 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001944
Clark Boylanb640e052014-04-03 16:41:46 -07001945 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001946 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1947 # see: https://github.com/jsocol/pystatsd/issues/61
1948 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001949 os.environ['STATSD_PORT'] = str(self.statsd.port)
1950 self.statsd.start()
1951 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001952 importlib.reload(statsd)
1953 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001954
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001955 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001956
1957 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001958 self.log.info("Gearman server on port %s" %
1959 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001960 if self.use_ssl:
1961 self.log.info('SSL enabled for gearman')
1962 self.config.set(
1963 'gearman', 'ssl_ca',
1964 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1965 self.config.set(
1966 'gearman', 'ssl_cert',
1967 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1968 self.config.set(
1969 'gearman', 'ssl_key',
1970 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001971
James E. Blaire511d2f2016-12-08 15:22:26 -08001972 gerritsource.GerritSource.replication_timeout = 1.5
1973 gerritsource.GerritSource.replication_retry_interval = 0.5
1974 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001975
Joshua Hesketh352264b2015-08-11 23:42:08 +10001976 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001977
Jan Hruban7083edd2015-08-21 14:00:54 +02001978 self.webapp = zuul.webapp.WebApp(
1979 self.sched, port=0, listen_address='127.0.0.1')
1980
Jan Hruban6b71aff2015-10-22 16:58:08 +02001981 self.event_queues = [
1982 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001983 self.sched.trigger_event_queue,
1984 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001985 ]
1986
James E. Blairfef78942016-03-11 16:28:56 -08001987 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001988 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001989
Paul Belanger174a8272017-03-14 13:20:10 -04001990 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001991 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001992 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001993 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001994 _test_root=self.test_root,
1995 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001996 self.executor_server.start()
1997 self.history = self.executor_server.build_history
1998 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001999
Paul Belanger174a8272017-03-14 13:20:10 -04002000 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002001 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002002 self.merge_client = zuul.merger.client.MergeClient(
2003 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002004 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002005 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002006 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002007
James E. Blair0d5a36e2017-02-21 10:53:44 -05002008 self.fake_nodepool = FakeNodepool(
2009 self.zk_chroot_fixture.zookeeper_host,
2010 self.zk_chroot_fixture.zookeeper_port,
2011 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002012
Paul Belanger174a8272017-03-14 13:20:10 -04002013 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002014 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002015 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002016 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002017
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002018 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002019
2020 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002021 self.webapp.start()
2022 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002023 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002024 # Cleanups are run in reverse order
2025 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002026 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002027 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002028
James E. Blairb9c0d772017-03-03 14:34:49 -08002029 self.sched.reconfigure(self.config)
2030 self.sched.resume()
2031
Tobias Henkel7df274b2017-05-26 17:41:11 +02002032 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002033 # Set up gerrit related fakes
2034 # Set a changes database so multiple FakeGerrit's can report back to
2035 # a virtual canonical database given by the configured hostname
2036 self.gerrit_changes_dbs = {}
2037
2038 def getGerritConnection(driver, name, config):
2039 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2040 con = FakeGerritConnection(driver, name, config,
2041 changes_db=db,
2042 upstream_root=self.upstream_root)
2043 self.event_queues.append(con.event_queue)
2044 setattr(self, 'fake_' + name, con)
2045 return con
2046
2047 self.useFixture(fixtures.MonkeyPatch(
2048 'zuul.driver.gerrit.GerritDriver.getConnection',
2049 getGerritConnection))
2050
Gregory Haynes4fc12542015-04-22 20:38:06 -07002051 def getGithubConnection(driver, name, config):
2052 con = FakeGithubConnection(driver, name, config,
2053 upstream_root=self.upstream_root)
2054 setattr(self, 'fake_' + name, con)
2055 return con
2056
2057 self.useFixture(fixtures.MonkeyPatch(
2058 'zuul.driver.github.GithubDriver.getConnection',
2059 getGithubConnection))
2060
James E. Blaire511d2f2016-12-08 15:22:26 -08002061 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002062 # TODO(jhesketh): This should come from lib.connections for better
2063 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002064 # Register connections from the config
2065 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002066
Joshua Hesketh352264b2015-08-11 23:42:08 +10002067 def FakeSMTPFactory(*args, **kw):
2068 args = [self.smtp_messages] + list(args)
2069 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002070
Joshua Hesketh352264b2015-08-11 23:42:08 +10002071 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002072
James E. Blaire511d2f2016-12-08 15:22:26 -08002073 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002074 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002075 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002076
James E. Blair83005782015-12-11 14:46:03 -08002077 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002078 # This creates the per-test configuration object. It can be
2079 # overriden by subclasses, but should not need to be since it
2080 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002081 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002082 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002083
James E. Blair39840362017-06-23 20:34:02 +01002084 sections = ['zuul', 'scheduler', 'executor', 'merger']
2085 for section in sections:
2086 if not self.config.has_section(section):
2087 self.config.add_section(section)
2088
James E. Blair06cc3922017-04-19 10:08:10 -07002089 if not self.setupSimpleLayout():
2090 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002091 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002092 self.tenant_config_file)
2093 git_path = os.path.join(
2094 os.path.dirname(
2095 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2096 'git')
2097 if os.path.exists(git_path):
2098 for reponame in os.listdir(git_path):
2099 project = reponame.replace('_', '/')
2100 self.copyDirToRepo(project,
2101 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002102 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002103 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002104 self.setupAllProjectKeys()
2105
James E. Blair06cc3922017-04-19 10:08:10 -07002106 def setupSimpleLayout(self):
2107 # If the test method has been decorated with a simple_layout,
2108 # use that instead of the class tenant_config_file. Set up a
2109 # single config-project with the specified layout, and
2110 # initialize repos for all of the 'project' entries which
2111 # appear in the layout.
2112 test_name = self.id().split('.')[-1]
2113 test = getattr(self, test_name)
2114 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002115 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002116 else:
2117 return False
2118
James E. Blairb70e55a2017-04-19 12:57:02 -07002119 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002120 path = os.path.join(FIXTURE_DIR, path)
2121 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002122 data = f.read()
2123 layout = yaml.safe_load(data)
2124 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002125 untrusted_projects = []
2126 for item in layout:
2127 if 'project' in item:
2128 name = item['project']['name']
2129 untrusted_projects.append(name)
2130 self.init_repo(name)
2131 self.addCommitToRepo(name, 'initial commit',
2132 files={'README': ''},
2133 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002134 if 'job' in item:
2135 jobname = item['job']['name']
2136 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002137
2138 root = os.path.join(self.test_root, "config")
2139 if not os.path.exists(root):
2140 os.makedirs(root)
2141 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2142 config = [{'tenant':
2143 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002144 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002145 {'config-projects': ['common-config'],
2146 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002147 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002148 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002149 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002150 os.path.join(FIXTURE_DIR, f.name))
2151
2152 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002153 self.addCommitToRepo('common-config', 'add content from fixture',
2154 files, branch='master', tag='init')
2155
2156 return True
2157
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002158 def setupAllProjectKeys(self):
2159 if self.create_project_keys:
2160 return
2161
James E. Blair39840362017-06-23 20:34:02 +01002162 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002163 with open(os.path.join(FIXTURE_DIR, path)) as f:
2164 tenant_config = yaml.safe_load(f.read())
2165 for tenant in tenant_config:
2166 sources = tenant['tenant']['source']
2167 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002168 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002169 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002170 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002171 self.setupProjectKeys(source, project)
2172
2173 def setupProjectKeys(self, source, project):
2174 # Make sure we set up an RSA key for the project so that we
2175 # don't spend time generating one:
2176
James E. Blair6459db12017-06-29 14:57:20 -07002177 if isinstance(project, dict):
2178 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002179 key_root = os.path.join(self.state_root, 'keys')
2180 if not os.path.isdir(key_root):
2181 os.mkdir(key_root, 0o700)
2182 private_key_file = os.path.join(key_root, source, project + '.pem')
2183 private_key_dir = os.path.dirname(private_key_file)
2184 self.log.debug("Installing test keys for project %s at %s" % (
2185 project, private_key_file))
2186 if not os.path.isdir(private_key_dir):
2187 os.makedirs(private_key_dir)
2188 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2189 with open(private_key_file, 'w') as o:
2190 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002191
James E. Blair498059b2016-12-20 13:50:13 -08002192 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002193 self.zk_chroot_fixture = self.useFixture(
2194 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002195 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002196 self.zk_chroot_fixture.zookeeper_host,
2197 self.zk_chroot_fixture.zookeeper_port,
2198 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002199
James E. Blair96c6bf82016-01-15 16:20:40 -08002200 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002201 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002202
2203 files = {}
2204 for (dirpath, dirnames, filenames) in os.walk(source_path):
2205 for filename in filenames:
2206 test_tree_filepath = os.path.join(dirpath, filename)
2207 common_path = os.path.commonprefix([test_tree_filepath,
2208 source_path])
2209 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2210 with open(test_tree_filepath, 'r') as f:
2211 content = f.read()
2212 files[relative_filepath] = content
2213 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002214 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002215
James E. Blaire18d4602017-01-05 11:17:28 -08002216 def assertNodepoolState(self):
2217 # Make sure that there are no pending requests
2218
2219 requests = self.fake_nodepool.getNodeRequests()
2220 self.assertEqual(len(requests), 0)
2221
2222 nodes = self.fake_nodepool.getNodes()
2223 for node in nodes:
2224 self.assertFalse(node['_lock'], "Node %s is locked" %
2225 (node['_oid'],))
2226
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002227 def assertNoGeneratedKeys(self):
2228 # Make sure that Zuul did not generate any project keys
2229 # (unless it was supposed to).
2230
2231 if self.create_project_keys:
2232 return
2233
2234 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2235 test_key = i.read()
2236
2237 key_root = os.path.join(self.state_root, 'keys')
2238 for root, dirname, files in os.walk(key_root):
2239 for fn in files:
2240 with open(os.path.join(root, fn)) as f:
2241 self.assertEqual(test_key, f.read())
2242
Clark Boylanb640e052014-04-03 16:41:46 -07002243 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002244 self.log.debug("Assert final state")
2245 # Make sure no jobs are running
2246 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002247 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002248 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002249 gc.collect()
2250 for obj in gc.get_objects():
2251 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002252 self.log.debug("Leaked git repo object: 0x%x %s" %
2253 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002254 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002255 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002256 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002257 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002258 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002259 for tenant in self.sched.abide.tenants.values():
2260 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002261 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002262 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002263
2264 def shutdown(self):
2265 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002266 self.executor_server.hold_jobs_in_build = False
2267 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002268 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002269 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002270 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002271 self.sched.stop()
2272 self.sched.join()
2273 self.statsd.stop()
2274 self.statsd.join()
2275 self.webapp.stop()
2276 self.webapp.join()
2277 self.rpc.stop()
2278 self.rpc.join()
2279 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002280 self.fake_nodepool.stop()
2281 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002282 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002283 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002284 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002285 # Further the pydevd threads also need to be whitelisted so debugging
2286 # e.g. in PyCharm is possible without breaking shutdown.
2287 whitelist = ['executor-watchdog',
2288 'pydevd.CommandThread',
2289 'pydevd.Reader',
2290 'pydevd.Writer',
2291 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002292 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002293 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002294 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002295 log_str = ""
2296 for thread_id, stack_frame in sys._current_frames().items():
2297 log_str += "Thread: %s\n" % thread_id
2298 log_str += "".join(traceback.format_stack(stack_frame))
2299 self.log.debug(log_str)
2300 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002301
James E. Blaira002b032017-04-18 10:35:48 -07002302 def assertCleanShutdown(self):
2303 pass
2304
James E. Blairc4ba97a2017-04-19 16:26:24 -07002305 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002306 parts = project.split('/')
2307 path = os.path.join(self.upstream_root, *parts[:-1])
2308 if not os.path.exists(path):
2309 os.makedirs(path)
2310 path = os.path.join(self.upstream_root, project)
2311 repo = git.Repo.init(path)
2312
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002313 with repo.config_writer() as config_writer:
2314 config_writer.set_value('user', 'email', 'user@example.com')
2315 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002316
Clark Boylanb640e052014-04-03 16:41:46 -07002317 repo.index.commit('initial commit')
2318 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002319 if tag:
2320 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002321
James E. Blair97d902e2014-08-21 13:25:56 -07002322 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002323 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002324 repo.git.clean('-x', '-f', '-d')
2325
James E. Blair97d902e2014-08-21 13:25:56 -07002326 def create_branch(self, project, branch):
2327 path = os.path.join(self.upstream_root, project)
2328 repo = git.Repo.init(path)
2329 fn = os.path.join(path, 'README')
2330
2331 branch_head = repo.create_head(branch)
2332 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002333 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002334 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002335 f.close()
2336 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002337 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002338
James E. Blair97d902e2014-08-21 13:25:56 -07002339 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002340 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002341 repo.git.clean('-x', '-f', '-d')
2342
Sachi King9f16d522016-03-16 12:20:45 +11002343 def create_commit(self, project):
2344 path = os.path.join(self.upstream_root, project)
2345 repo = git.Repo(path)
2346 repo.head.reference = repo.heads['master']
2347 file_name = os.path.join(path, 'README')
2348 with open(file_name, 'a') as f:
2349 f.write('creating fake commit\n')
2350 repo.index.add([file_name])
2351 commit = repo.index.commit('Creating a fake commit')
2352 return commit.hexsha
2353
James E. Blairf4a5f022017-04-18 14:01:10 -07002354 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002355 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002356 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002357 while len(self.builds):
2358 self.release(self.builds[0])
2359 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002360 i += 1
2361 if count is not None and i >= count:
2362 break
James E. Blairb8c16472015-05-05 14:55:26 -07002363
James E. Blairdf25ddc2017-07-08 07:57:09 -07002364 def getSortedBuilds(self):
2365 "Return the list of currently running builds sorted by name"
2366
2367 return sorted(self.builds, key=lambda x: x.name)
2368
Clark Boylanb640e052014-04-03 16:41:46 -07002369 def release(self, job):
2370 if isinstance(job, FakeBuild):
2371 job.release()
2372 else:
2373 job.waiting = False
2374 self.log.debug("Queued job %s released" % job.unique)
2375 self.gearman_server.wakeConnections()
2376
2377 def getParameter(self, job, name):
2378 if isinstance(job, FakeBuild):
2379 return job.parameters[name]
2380 else:
2381 parameters = json.loads(job.arguments)
2382 return parameters[name]
2383
Clark Boylanb640e052014-04-03 16:41:46 -07002384 def haveAllBuildsReported(self):
2385 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002386 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002387 return False
2388 # Find out if every build that the worker has completed has been
2389 # reported back to Zuul. If it hasn't then that means a Gearman
2390 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002391 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002392 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002393 if not zbuild:
2394 # It has already been reported
2395 continue
2396 # It hasn't been reported yet.
2397 return False
2398 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002399 worker = self.executor_server.executor_worker
2400 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002401 if connection.state == 'GRAB_WAIT':
2402 return False
2403 return True
2404
2405 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002406 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002407 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002408 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002409 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002410 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002411 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002412 for j in conn.related_jobs.values():
2413 if j.unique == build.uuid:
2414 client_job = j
2415 break
2416 if not client_job:
2417 self.log.debug("%s is not known to the gearman client" %
2418 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002419 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002420 if not client_job.handle:
2421 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002422 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002423 server_job = self.gearman_server.jobs.get(client_job.handle)
2424 if not server_job:
2425 self.log.debug("%s is not known to the gearman server" %
2426 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002427 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002428 if not hasattr(server_job, 'waiting'):
2429 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002430 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002431 if server_job.waiting:
2432 continue
James E. Blair17302972016-08-10 16:11:42 -07002433 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002434 self.log.debug("%s has not reported start" % build)
2435 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002436 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002437 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002438 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002439 if worker_build:
2440 if worker_build.isWaiting():
2441 continue
2442 else:
2443 self.log.debug("%s is running" % worker_build)
2444 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002445 else:
James E. Blair962220f2016-08-03 11:22:38 -07002446 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002447 return False
James E. Blaira002b032017-04-18 10:35:48 -07002448 for (build_uuid, job_worker) in \
2449 self.executor_server.job_workers.items():
2450 if build_uuid not in seen_builds:
2451 self.log.debug("%s is not finalized" % build_uuid)
2452 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002453 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002454
James E. Blairdce6cea2016-12-20 16:45:32 -08002455 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002456 if self.fake_nodepool.paused:
2457 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002458 if self.sched.nodepool.requests:
2459 return False
2460 return True
2461
Jan Hruban6b71aff2015-10-22 16:58:08 +02002462 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002463 for event_queue in self.event_queues:
2464 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002465
2466 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002467 for event_queue in self.event_queues:
2468 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002469
Clark Boylanb640e052014-04-03 16:41:46 -07002470 def waitUntilSettled(self):
2471 self.log.debug("Waiting until settled...")
2472 start = time.time()
2473 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002474 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002475 self.log.error("Timeout waiting for Zuul to settle")
2476 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002477 for event_queue in self.event_queues:
2478 self.log.error(" %s: %s" %
2479 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002480 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002481 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002482 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002483 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002484 self.log.error("All requests completed: %s" %
2485 (self.areAllNodeRequestsComplete(),))
2486 self.log.error("Merge client jobs: %s" %
2487 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002488 raise Exception("Timeout waiting for Zuul to settle")
2489 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002490
Paul Belanger174a8272017-03-14 13:20:10 -04002491 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002492 # have all build states propogated to zuul?
2493 if self.haveAllBuildsReported():
2494 # Join ensures that the queue is empty _and_ events have been
2495 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002496 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002497 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002498 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002499 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002500 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002501 self.areAllNodeRequestsComplete() and
2502 all(self.eventQueuesEmpty())):
2503 # The queue empty check is placed at the end to
2504 # ensure that if a component adds an event between
2505 # when locked the run handler and checked that the
2506 # components were stable, we don't erroneously
2507 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002508 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002509 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002510 self.log.debug("...settled.")
2511 return
2512 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002513 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002514 self.sched.wake_event.wait(0.1)
2515
2516 def countJobResults(self, jobs, result):
2517 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002518 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002519
Monty Taylor0d926122017-05-24 08:07:56 -05002520 def getBuildByName(self, name):
2521 for build in self.builds:
2522 if build.name == name:
2523 return build
2524 raise Exception("Unable to find build %s" % name)
2525
James E. Blair96c6bf82016-01-15 16:20:40 -08002526 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002527 for job in self.history:
2528 if (job.name == name and
2529 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002530 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002531 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002532 raise Exception("Unable to find job %s in history" % name)
2533
2534 def assertEmptyQueues(self):
2535 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002536 for tenant in self.sched.abide.tenants.values():
2537 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002538 for pipeline_queue in pipeline.queues:
2539 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002540 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002541 pipeline.name, pipeline_queue.name,
2542 pipeline_queue.queue))
2543 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002544 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002545
2546 def assertReportedStat(self, key, value=None, kind=None):
2547 start = time.time()
2548 while time.time() < (start + 5):
2549 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002550 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002551 if key == k:
2552 if value is None and kind is None:
2553 return
2554 elif value:
2555 if value == v:
2556 return
2557 elif kind:
2558 if v.endswith('|' + kind):
2559 return
2560 time.sleep(0.1)
2561
Clark Boylanb640e052014-04-03 16:41:46 -07002562 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002563
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002564 def assertBuilds(self, builds):
2565 """Assert that the running builds are as described.
2566
2567 The list of running builds is examined and must match exactly
2568 the list of builds described by the input.
2569
2570 :arg list builds: A list of dictionaries. Each item in the
2571 list must match the corresponding build in the build
2572 history, and each element of the dictionary must match the
2573 corresponding attribute of the build.
2574
2575 """
James E. Blair3158e282016-08-19 09:34:11 -07002576 try:
2577 self.assertEqual(len(self.builds), len(builds))
2578 for i, d in enumerate(builds):
2579 for k, v in d.items():
2580 self.assertEqual(
2581 getattr(self.builds[i], k), v,
2582 "Element %i in builds does not match" % (i,))
2583 except Exception:
2584 for build in self.builds:
2585 self.log.error("Running build: %s" % build)
2586 else:
2587 self.log.error("No running builds")
2588 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002589
James E. Blairb536ecc2016-08-31 10:11:42 -07002590 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002591 """Assert that the completed builds are as described.
2592
2593 The list of completed builds is examined and must match
2594 exactly the list of builds described by the input.
2595
2596 :arg list history: A list of dictionaries. Each item in the
2597 list must match the corresponding build in the build
2598 history, and each element of the dictionary must match the
2599 corresponding attribute of the build.
2600
James E. Blairb536ecc2016-08-31 10:11:42 -07002601 :arg bool ordered: If true, the history must match the order
2602 supplied, if false, the builds are permitted to have
2603 arrived in any order.
2604
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002605 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002606 def matches(history_item, item):
2607 for k, v in item.items():
2608 if getattr(history_item, k) != v:
2609 return False
2610 return True
James E. Blair3158e282016-08-19 09:34:11 -07002611 try:
2612 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002613 if ordered:
2614 for i, d in enumerate(history):
2615 if not matches(self.history[i], d):
2616 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002617 "Element %i in history does not match %s" %
2618 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002619 else:
2620 unseen = self.history[:]
2621 for i, d in enumerate(history):
2622 found = False
2623 for unseen_item in unseen:
2624 if matches(unseen_item, d):
2625 found = True
2626 unseen.remove(unseen_item)
2627 break
2628 if not found:
2629 raise Exception("No match found for element %i "
2630 "in history" % (i,))
2631 if unseen:
2632 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002633 except Exception:
2634 for build in self.history:
2635 self.log.error("Completed build: %s" % build)
2636 else:
2637 self.log.error("No completed builds")
2638 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002639
James E. Blair6ac368c2016-12-22 18:07:20 -08002640 def printHistory(self):
2641 """Log the build history.
2642
2643 This can be useful during tests to summarize what jobs have
2644 completed.
2645
2646 """
2647 self.log.debug("Build history:")
2648 for build in self.history:
2649 self.log.debug(build)
2650
James E. Blair59fdbac2015-12-07 17:08:06 -08002651 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002652 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2653
James E. Blair9ea70072017-04-19 16:05:30 -07002654 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002655 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002656 if not os.path.exists(root):
2657 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002658 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2659 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002660- tenant:
2661 name: openstack
2662 source:
2663 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002664 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002665 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002666 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002667 - org/project
2668 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002669 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002670 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002671 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002672 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002673 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002674
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002675 def addCommitToRepo(self, project, message, files,
2676 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002677 path = os.path.join(self.upstream_root, project)
2678 repo = git.Repo(path)
2679 repo.head.reference = branch
2680 zuul.merger.merger.reset_repo_to_head(repo)
2681 for fn, content in files.items():
2682 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002683 try:
2684 os.makedirs(os.path.dirname(fn))
2685 except OSError:
2686 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002687 with open(fn, 'w') as f:
2688 f.write(content)
2689 repo.index.add([fn])
2690 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002691 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002692 repo.heads[branch].commit = commit
2693 repo.head.reference = branch
2694 repo.git.clean('-x', '-f', '-d')
2695 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002696 if tag:
2697 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002698 return before
2699
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002700 def commitConfigUpdate(self, project_name, source_name):
2701 """Commit an update to zuul.yaml
2702
2703 This overwrites the zuul.yaml in the specificed project with
2704 the contents specified.
2705
2706 :arg str project_name: The name of the project containing
2707 zuul.yaml (e.g., common-config)
2708
2709 :arg str source_name: The path to the file (underneath the
2710 test fixture directory) whose contents should be used to
2711 replace zuul.yaml.
2712 """
2713
2714 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002715 files = {}
2716 with open(source_path, 'r') as f:
2717 data = f.read()
2718 layout = yaml.safe_load(data)
2719 files['zuul.yaml'] = data
2720 for item in layout:
2721 if 'job' in item:
2722 jobname = item['job']['name']
2723 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002724 before = self.addCommitToRepo(
2725 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002726 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002727 return before
2728
James E. Blair7fc8daa2016-08-08 15:37:15 -07002729 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002730
James E. Blair7fc8daa2016-08-08 15:37:15 -07002731 """Inject a Fake (Gerrit) event.
2732
2733 This method accepts a JSON-encoded event and simulates Zuul
2734 having received it from Gerrit. It could (and should)
2735 eventually apply to any connection type, but is currently only
2736 used with Gerrit connections. The name of the connection is
2737 used to look up the corresponding server, and the event is
2738 simulated as having been received by all Zuul connections
2739 attached to that server. So if two Gerrit connections in Zuul
2740 are connected to the same Gerrit server, and you invoke this
2741 method specifying the name of one of them, the event will be
2742 received by both.
2743
2744 .. note::
2745
2746 "self.fake_gerrit.addEvent" calls should be migrated to
2747 this method.
2748
2749 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002750 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002751 :arg str event: The JSON-encoded event.
2752
2753 """
2754 specified_conn = self.connections.connections[connection]
2755 for conn in self.connections.connections.values():
2756 if (isinstance(conn, specified_conn.__class__) and
2757 specified_conn.server == conn.server):
2758 conn.addEvent(event)
2759
James E. Blaird8af5422017-05-24 13:59:40 -07002760 def getUpstreamRepos(self, projects):
2761 """Return upstream git repo objects for the listed projects
2762
2763 :arg list projects: A list of strings, each the canonical name
2764 of a project.
2765
2766 :returns: A dictionary of {name: repo} for every listed
2767 project.
2768 :rtype: dict
2769
2770 """
2771
2772 repos = {}
2773 for project in projects:
2774 # FIXME(jeblair): the upstream root does not yet have a
2775 # hostname component; that needs to be added, and this
2776 # line removed:
2777 tmp_project_name = '/'.join(project.split('/')[1:])
2778 path = os.path.join(self.upstream_root, tmp_project_name)
2779 repo = git.Repo(path)
2780 repos[project] = repo
2781 return repos
2782
James E. Blair3f876d52016-07-22 13:07:14 -07002783
2784class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002785 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002786 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002787
Jamie Lennox7655b552017-03-17 12:33:38 +11002788 @contextmanager
2789 def jobLog(self, build):
2790 """Print job logs on assertion errors
2791
2792 This method is a context manager which, if it encounters an
2793 ecxeption, adds the build log to the debug output.
2794
2795 :arg Build build: The build that's being asserted.
2796 """
2797 try:
2798 yield
2799 except Exception:
2800 path = os.path.join(self.test_root, build.uuid,
2801 'work', 'logs', 'job-output.txt')
2802 with open(path) as f:
2803 self.log.debug(f.read())
2804 raise
2805
Joshua Heskethd78b4482015-09-14 16:56:34 -06002806
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002807class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002808 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002809 use_ssl = True
2810
2811
Joshua Heskethd78b4482015-09-14 16:56:34 -06002812class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002813 def setup_config(self):
2814 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002815 for section_name in self.config.sections():
2816 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2817 section_name, re.I)
2818 if not con_match:
2819 continue
2820
2821 if self.config.get(section_name, 'driver') == 'sql':
2822 f = MySQLSchemaFixture()
2823 self.useFixture(f)
2824 if (self.config.get(section_name, 'dburi') ==
2825 '$MYSQL_FIXTURE_DBURI$'):
2826 self.config.set(section_name, 'dburi', f.dburi)