blob: 0f188bd2f0de7d504e9dfb2d01904554c96f4e90 [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
Tobias Henkeleca46202017-08-02 20:27:10 +0200570 def branches(self, protected=False):
571 if protected:
572 # simulate there is no protected branch
573 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200574 return self._branches
575
Tobias Henkel64e37a02017-08-02 10:13:30 +0200576 def user(self, login):
577 return self.FakeUser(login)
578
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200579 def repository(self, owner, proj):
580 repo = self.FakeRepository()
581 return repo
582
Tobias Henkel64e37a02017-08-02 10:13:30 +0200583
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584class FakeGithubPullRequest(object):
585
586 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800587 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700588 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 """Creates a new PR with several commits.
590 Sends an event about opened PR."""
591 self.github = github
592 self.source = github
593 self.number = number
594 self.project = project
595 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100596 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700597 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100598 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700599 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100600 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700601 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100602 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100603 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800604 self.reviews = []
605 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700606 self.updated_at = None
607 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100608 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100609 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700610 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700611 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100612 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700613 self._updateTimeStamp()
614
Jan Hruban570d01c2016-03-10 21:51:32 +0100615 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700616 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100617 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700618 self._updateTimeStamp()
619
Jan Hruban570d01c2016-03-10 21:51:32 +0100620 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700621 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100622 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700623 self._updateTimeStamp()
624
625 def getPullRequestOpenedEvent(self):
626 return self._getPullRequestEvent('opened')
627
628 def getPullRequestSynchronizeEvent(self):
629 return self._getPullRequestEvent('synchronize')
630
631 def getPullRequestReopenedEvent(self):
632 return self._getPullRequestEvent('reopened')
633
634 def getPullRequestClosedEvent(self):
635 return self._getPullRequestEvent('closed')
636
Jesse Keatinga41566f2017-06-14 18:17:51 -0700637 def getPullRequestEditedEvent(self):
638 return self._getPullRequestEvent('edited')
639
Gregory Haynes4fc12542015-04-22 20:38:06 -0700640 def addComment(self, message):
641 self.comments.append(message)
642 self._updateTimeStamp()
643
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200644 def getCommentAddedEvent(self, text):
645 name = 'issue_comment'
646 data = {
647 'action': 'created',
648 'issue': {
649 'number': self.number
650 },
651 'comment': {
652 'body': text
653 },
654 'repository': {
655 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100656 },
657 'sender': {
658 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200659 }
660 }
661 return (name, data)
662
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800663 def getReviewAddedEvent(self, review):
664 name = 'pull_request_review'
665 data = {
666 'action': 'submitted',
667 'pull_request': {
668 'number': self.number,
669 'title': self.subject,
670 'updated_at': self.updated_at,
671 'base': {
672 'ref': self.branch,
673 'repo': {
674 'full_name': self.project
675 }
676 },
677 'head': {
678 'sha': self.head_sha
679 }
680 },
681 'review': {
682 'state': review
683 },
684 'repository': {
685 'full_name': self.project
686 },
687 'sender': {
688 'login': 'ghuser'
689 }
690 }
691 return (name, data)
692
Jan Hruban16ad31f2015-11-07 14:39:07 +0100693 def addLabel(self, name):
694 if name not in self.labels:
695 self.labels.append(name)
696 self._updateTimeStamp()
697 return self._getLabelEvent(name)
698
699 def removeLabel(self, name):
700 if name in self.labels:
701 self.labels.remove(name)
702 self._updateTimeStamp()
703 return self._getUnlabelEvent(name)
704
705 def _getLabelEvent(self, label):
706 name = 'pull_request'
707 data = {
708 'action': 'labeled',
709 'pull_request': {
710 'number': self.number,
711 'updated_at': self.updated_at,
712 'base': {
713 'ref': self.branch,
714 'repo': {
715 'full_name': self.project
716 }
717 },
718 'head': {
719 'sha': self.head_sha
720 }
721 },
722 'label': {
723 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100724 },
725 'sender': {
726 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100727 }
728 }
729 return (name, data)
730
731 def _getUnlabelEvent(self, label):
732 name = 'pull_request'
733 data = {
734 'action': 'unlabeled',
735 'pull_request': {
736 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100737 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100738 'updated_at': self.updated_at,
739 'base': {
740 'ref': self.branch,
741 'repo': {
742 'full_name': self.project
743 }
744 },
745 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800746 'sha': self.head_sha,
747 'repo': {
748 'full_name': self.project
749 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100750 }
751 },
752 'label': {
753 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100754 },
755 'sender': {
756 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100757 }
758 }
759 return (name, data)
760
Jesse Keatinga41566f2017-06-14 18:17:51 -0700761 def editBody(self, body):
762 self.body = body
763 self._updateTimeStamp()
764
Gregory Haynes4fc12542015-04-22 20:38:06 -0700765 def _getRepo(self):
766 repo_path = os.path.join(self.upstream_root, self.project)
767 return git.Repo(repo_path)
768
769 def _createPRRef(self):
770 repo = self._getRepo()
771 GithubChangeReference.create(
772 repo, self._getPRReference(), 'refs/tags/init')
773
Jan Hruban570d01c2016-03-10 21:51:32 +0100774 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700775 repo = self._getRepo()
776 ref = repo.references[self._getPRReference()]
777 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100778 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700779 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100780 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700781 repo.head.reference = ref
782 zuul.merger.merger.reset_repo_to_head(repo)
783 repo.git.clean('-x', '-f', '-d')
784
Jan Hruban570d01c2016-03-10 21:51:32 +0100785 if files:
786 fn = files[0]
787 self.files = files
788 else:
789 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
790 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100791 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700792 fn = os.path.join(repo.working_dir, fn)
793 f = open(fn, 'w')
794 with open(fn, 'w') as f:
795 f.write("test %s %s\n" %
796 (self.branch, self.number))
797 repo.index.add([fn])
798
799 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800800 # Create an empty set of statuses for the given sha,
801 # each sha on a PR may have a status set on it
802 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700803 repo.head.reference = 'master'
804 zuul.merger.merger.reset_repo_to_head(repo)
805 repo.git.clean('-x', '-f', '-d')
806 repo.heads['master'].checkout()
807
808 def _updateTimeStamp(self):
809 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
810
811 def getPRHeadSha(self):
812 repo = self._getRepo()
813 return repo.references[self._getPRReference()].commit.hexsha
814
Jesse Keatingae4cd272017-01-30 17:10:44 -0800815 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800816 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
817 # convert the timestamp to a str format that would be returned
818 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800819
Adam Gandelmand81dd762017-02-09 15:15:49 -0800820 if granted_on:
821 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
822 submitted_at = time.strftime(
823 gh_time_format, granted_on.timetuple())
824 else:
825 # github timestamps only down to the second, so we need to make
826 # sure reviews that tests add appear to be added over a period of
827 # time in the past and not all at once.
828 if not self.reviews:
829 # the first review happens 10 mins ago
830 offset = 600
831 else:
832 # subsequent reviews happen 1 minute closer to now
833 offset = 600 - (len(self.reviews) * 60)
834
835 granted_on = datetime.datetime.utcfromtimestamp(
836 time.time() - offset)
837 submitted_at = time.strftime(
838 gh_time_format, granted_on.timetuple())
839
Jesse Keatingae4cd272017-01-30 17:10:44 -0800840 self.reviews.append({
841 'state': state,
842 'user': {
843 'login': user,
844 'email': user + "@derp.com",
845 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800846 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800847 })
848
Gregory Haynes4fc12542015-04-22 20:38:06 -0700849 def _getPRReference(self):
850 return '%s/head' % self.number
851
852 def _getPullRequestEvent(self, action):
853 name = 'pull_request'
854 data = {
855 'action': action,
856 'number': self.number,
857 'pull_request': {
858 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100859 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700860 'updated_at': self.updated_at,
861 'base': {
862 'ref': self.branch,
863 'repo': {
864 'full_name': self.project
865 }
866 },
867 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800868 'sha': self.head_sha,
869 'repo': {
870 'full_name': self.project
871 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700872 },
873 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100874 },
875 'sender': {
876 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 }
878 }
879 return (name, data)
880
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800881 def getCommitStatusEvent(self, context, state='success', user='zuul'):
882 name = 'status'
883 data = {
884 'state': state,
885 'sha': self.head_sha,
886 'description': 'Test results for %s: %s' % (self.head_sha, state),
887 'target_url': 'http://zuul/%s' % self.head_sha,
888 'branches': [],
889 'context': context,
890 'sender': {
891 'login': user
892 }
893 }
894 return (name, data)
895
Gregory Haynes4fc12542015-04-22 20:38:06 -0700896
897class FakeGithubConnection(githubconnection.GithubConnection):
898 log = logging.getLogger("zuul.test.FakeGithubConnection")
899
900 def __init__(self, driver, connection_name, connection_config,
901 upstream_root=None):
902 super(FakeGithubConnection, self).__init__(driver, connection_name,
903 connection_config)
904 self.connection_name = connection_name
905 self.pr_number = 0
906 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700907 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700908 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100909 self.merge_failure = False
910 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100911 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +0200912 self.github_client = FakeGithub()
913
914 def getGithubClient(self,
915 project=None,
916 user_id=None,
917 use_app=True):
918 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700919
Jesse Keatinga41566f2017-06-14 18:17:51 -0700920 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700921 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700922 self.pr_number += 1
923 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100924 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700925 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926 self.pull_requests.append(pull_request)
927 return pull_request
928
Jesse Keating71a47ff2017-06-06 11:36:43 -0700929 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
930 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700931 if not old_rev:
932 old_rev = '00000000000000000000000000000000'
933 if not new_rev:
934 new_rev = random_sha1()
935 name = 'push'
936 data = {
937 'ref': ref,
938 'before': old_rev,
939 'after': new_rev,
940 'repository': {
941 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700942 },
943 'commits': [
944 {
945 'added': added_files,
946 'removed': removed_files,
947 'modified': modified_files
948 }
949 ]
Wayne1a78c612015-06-11 17:14:13 -0700950 }
951 return (name, data)
952
Gregory Haynes4fc12542015-04-22 20:38:06 -0700953 def emitEvent(self, event):
954 """Emulates sending the GitHub webhook event to the connection."""
955 port = self.webapp.server.socket.getsockname()[1]
956 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700957 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -0700958 secret = self.connection_config['webhook_token']
959 signature = githubconnection._sign_request(payload, secret)
960 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700961 req = urllib.request.Request(
962 'http://localhost:%s/connection/%s/payload'
963 % (port, self.connection_name),
964 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +0000965 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700966
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200967 def getPull(self, project, number):
968 pr = self.pull_requests[number - 1]
969 data = {
970 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100971 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200972 'updated_at': pr.updated_at,
973 'base': {
974 'repo': {
975 'full_name': pr.project
976 },
977 'ref': pr.branch,
978 },
Jan Hruban37615e52015-11-19 14:30:49 +0100979 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700980 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200981 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800982 'sha': pr.head_sha,
983 'repo': {
984 'full_name': pr.project
985 }
Jesse Keating61040e72017-06-08 15:08:27 -0700986 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700987 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700988 'labels': pr.labels,
989 'merged': pr.is_merged,
990 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200991 }
992 return data
993
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800994 def getPullBySha(self, sha):
995 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
996 if len(prs) > 1:
997 raise Exception('Multiple pulls found with head sha: %s' % sha)
998 pr = prs[0]
999 return self.getPull(pr.project, pr.number)
1000
Jesse Keatingae4cd272017-01-30 17:10:44 -08001001 def _getPullReviews(self, owner, project, number):
1002 pr = self.pull_requests[number - 1]
1003 return pr.reviews
1004
Jesse Keatingae4cd272017-01-30 17:10:44 -08001005 def getRepoPermission(self, project, login):
1006 owner, proj = project.split('/')
1007 for pr in self.pull_requests:
1008 pr_owner, pr_project = pr.project.split('/')
1009 if (pr_owner == owner and proj == pr_project):
1010 if login in pr.writers:
1011 return 'write'
1012 else:
1013 return 'read'
1014
Gregory Haynes4fc12542015-04-22 20:38:06 -07001015 def getGitUrl(self, project):
1016 return os.path.join(self.upstream_root, str(project))
1017
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001018 def real_getGitUrl(self, project):
1019 return super(FakeGithubConnection, self).getGitUrl(project)
1020
Jan Hrubane252a732017-01-03 15:03:09 +01001021 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001022 # record that this got reported
1023 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001024 pull_request = self.pull_requests[pr_number - 1]
1025 pull_request.addComment(message)
1026
Jan Hruban3b415922016-02-03 13:10:22 +01001027 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001028 # record that this got reported
1029 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001030 pull_request = self.pull_requests[pr_number - 1]
1031 if self.merge_failure:
1032 raise Exception('Pull request was not merged')
1033 if self.merge_not_allowed_count > 0:
1034 self.merge_not_allowed_count -= 1
1035 raise MergeFailure('Merge was not successful due to mergeability'
1036 ' conflict')
1037 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001038 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001039
Jesse Keatingd96e5882017-01-19 13:55:50 -08001040 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001041 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001042
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001043 def setCommitStatus(self, project, sha, state, url='', description='',
1044 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001045 # record that this got reported
1046 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001047 # always insert a status to the front of the list, to represent
1048 # the last status provided for a commit.
1049 # Since we're bypassing github API, which would require a user, we
1050 # default the user as 'zuul' here.
1051 self.statuses.setdefault(project, {}).setdefault(sha, [])
1052 self.statuses[project][sha].insert(0, {
1053 'state': state,
1054 'url': url,
1055 'description': description,
1056 'context': context,
1057 'creator': {
1058 'login': user
1059 }
1060 })
Jan Hrubane252a732017-01-03 15:03:09 +01001061
Jan Hruban16ad31f2015-11-07 14:39:07 +01001062 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001063 # record that this got reported
1064 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001065 pull_request = self.pull_requests[pr_number - 1]
1066 pull_request.addLabel(label)
1067
1068 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001069 # record that this got reported
1070 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001071 pull_request = self.pull_requests[pr_number - 1]
1072 pull_request.removeLabel(label)
1073
Jesse Keatinga41566f2017-06-14 18:17:51 -07001074 def _getNeededByFromPR(self, change):
1075 prs = []
1076 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001077 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001078 change.number))
1079 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001080 if not pr.body:
1081 body = ''
1082 else:
1083 body = pr.body
1084 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001085 # Get our version of a pull so that it's a dict
1086 pull = self.getPull(pr.project, pr.number)
1087 prs.append(pull)
1088
1089 return prs
1090
Gregory Haynes4fc12542015-04-22 20:38:06 -07001091
Clark Boylanb640e052014-04-03 16:41:46 -07001092class BuildHistory(object):
1093 def __init__(self, **kw):
1094 self.__dict__.update(kw)
1095
1096 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001097 return ("<Completed build, result: %s name: %s uuid: %s "
1098 "changes: %s ref: %s>" %
1099 (self.result, self.name, self.uuid,
1100 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001101
1102
Clark Boylanb640e052014-04-03 16:41:46 -07001103class FakeStatsd(threading.Thread):
1104 def __init__(self):
1105 threading.Thread.__init__(self)
1106 self.daemon = True
1107 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1108 self.sock.bind(('', 0))
1109 self.port = self.sock.getsockname()[1]
1110 self.wake_read, self.wake_write = os.pipe()
1111 self.stats = []
1112
1113 def run(self):
1114 while True:
1115 poll = select.poll()
1116 poll.register(self.sock, select.POLLIN)
1117 poll.register(self.wake_read, select.POLLIN)
1118 ret = poll.poll()
1119 for (fd, event) in ret:
1120 if fd == self.sock.fileno():
1121 data = self.sock.recvfrom(1024)
1122 if not data:
1123 return
1124 self.stats.append(data[0])
1125 if fd == self.wake_read:
1126 return
1127
1128 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001129 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001130
1131
James E. Blaire1767bc2016-08-02 10:00:27 -07001132class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001133 log = logging.getLogger("zuul.test")
1134
Paul Belanger174a8272017-03-14 13:20:10 -04001135 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001136 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001137 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001139 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001140 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001141 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001142 # TODOv3(jeblair): self.node is really "the label of the node
1143 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001144 # keep using it like this, or we may end up exposing more of
1145 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001146 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001147 self.node = None
1148 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001149 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001150 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001151 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001152 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001153 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001154 self.wait_condition = threading.Condition()
1155 self.waiting = False
1156 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001157 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001158 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001159 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001160 items = self.parameters['zuul']['items']
1161 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1162 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001163
James E. Blair3158e282016-08-19 09:34:11 -07001164 def __repr__(self):
1165 waiting = ''
1166 if self.waiting:
1167 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001168 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1169 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001170
Clark Boylanb640e052014-04-03 16:41:46 -07001171 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001172 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001173 self.wait_condition.acquire()
1174 self.wait_condition.notify()
1175 self.waiting = False
1176 self.log.debug("Build %s released" % self.unique)
1177 self.wait_condition.release()
1178
1179 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001180 """Return whether this build is being held.
1181
1182 :returns: Whether the build is being held.
1183 :rtype: bool
1184 """
1185
Clark Boylanb640e052014-04-03 16:41:46 -07001186 self.wait_condition.acquire()
1187 if self.waiting:
1188 ret = True
1189 else:
1190 ret = False
1191 self.wait_condition.release()
1192 return ret
1193
1194 def _wait(self):
1195 self.wait_condition.acquire()
1196 self.waiting = True
1197 self.log.debug("Build %s waiting" % self.unique)
1198 self.wait_condition.wait()
1199 self.wait_condition.release()
1200
1201 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001202 self.log.debug('Running build %s' % self.unique)
1203
Paul Belanger174a8272017-03-14 13:20:10 -04001204 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001205 self.log.debug('Holding build %s' % self.unique)
1206 self._wait()
1207 self.log.debug("Build %s continuing" % self.unique)
1208
James E. Blair412fba82017-01-26 15:00:50 -08001209 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001210 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001211 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001212 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001213 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001214 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001215 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001216
James E. Blaire1767bc2016-08-02 10:00:27 -07001217 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001218
James E. Blaira5dba232016-08-08 15:53:24 -07001219 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001220 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001221 for change in changes:
1222 if self.hasChanges(change):
1223 return True
1224 return False
1225
James E. Blaire7b99a02016-08-05 14:27:34 -07001226 def hasChanges(self, *changes):
1227 """Return whether this build has certain changes in its git repos.
1228
1229 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001230 are expected to be present (in order) in the git repository of
1231 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001232
1233 :returns: Whether the build has the indicated changes.
1234 :rtype: bool
1235
1236 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001237 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001238 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001239 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001240 try:
1241 repo = git.Repo(path)
1242 except NoSuchPathError as e:
1243 self.log.debug('%s' % e)
1244 return False
James E. Blair247cab72017-07-20 16:52:36 -07001245 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001246 commit_message = '%s-1' % change.subject
1247 self.log.debug("Checking if build %s has changes; commit_message "
1248 "%s; repo_messages %s" % (self, commit_message,
1249 repo_messages))
1250 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001251 self.log.debug(" messages do not match")
1252 return False
1253 self.log.debug(" OK")
1254 return True
1255
James E. Blaird8af5422017-05-24 13:59:40 -07001256 def getWorkspaceRepos(self, projects):
1257 """Return workspace git repo objects for the listed projects
1258
1259 :arg list projects: A list of strings, each the canonical name
1260 of a project.
1261
1262 :returns: A dictionary of {name: repo} for every listed
1263 project.
1264 :rtype: dict
1265
1266 """
1267
1268 repos = {}
1269 for project in projects:
1270 path = os.path.join(self.jobdir.src_root, project)
1271 repo = git.Repo(path)
1272 repos[project] = repo
1273 return repos
1274
Clark Boylanb640e052014-04-03 16:41:46 -07001275
Paul Belanger174a8272017-03-14 13:20:10 -04001276class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1277 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001278
Paul Belanger174a8272017-03-14 13:20:10 -04001279 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001280 they will report that they have started but then pause until
1281 released before reporting completion. This attribute may be
1282 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001283 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001284 be explicitly released.
1285
1286 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001287 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001289 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001290 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001291 self.hold_jobs_in_build = False
1292 self.lock = threading.Lock()
1293 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001294 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001295 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001296 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001297
James E. Blaira5dba232016-08-08 15:53:24 -07001298 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001299 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001300
1301 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001302 :arg Change change: The :py:class:`~tests.base.FakeChange`
1303 instance which should cause the job to fail. This job
1304 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001305
1306 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001307 l = self.fail_tests.get(name, [])
1308 l.append(change)
1309 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001310
James E. Blair962220f2016-08-03 11:22:38 -07001311 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001312 """Release a held build.
1313
1314 :arg str regex: A regular expression which, if supplied, will
1315 cause only builds with matching names to be released. If
1316 not supplied, all builds will be released.
1317
1318 """
James E. Blair962220f2016-08-03 11:22:38 -07001319 builds = self.running_builds[:]
1320 self.log.debug("Releasing build %s (%s)" % (regex,
1321 len(self.running_builds)))
1322 for build in builds:
1323 if not regex or re.match(regex, build.name):
1324 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001325 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001326 build.release()
1327 else:
1328 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001329 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001330 self.log.debug("Done releasing builds %s (%s)" %
1331 (regex, len(self.running_builds)))
1332
Paul Belanger174a8272017-03-14 13:20:10 -04001333 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001334 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001335 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001336 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001337 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001338 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001339 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001340 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001341 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1342 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001343
1344 def stopJob(self, job):
1345 self.log.debug("handle stop")
1346 parameters = json.loads(job.arguments)
1347 uuid = parameters['uuid']
1348 for build in self.running_builds:
1349 if build.unique == uuid:
1350 build.aborted = True
1351 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001352 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001353
James E. Blaira002b032017-04-18 10:35:48 -07001354 def stop(self):
1355 for build in self.running_builds:
1356 build.release()
1357 super(RecordingExecutorServer, self).stop()
1358
Joshua Hesketh50c21782016-10-13 21:34:14 +11001359
Paul Belanger174a8272017-03-14 13:20:10 -04001360class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001361 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001362 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001363 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001364 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001365 if not commit: # merge conflict
1366 self.recordResult('MERGER_FAILURE')
1367 return commit
1368
1369 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001370 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001371 self.executor_server.lock.acquire()
1372 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001373 BuildHistory(name=build.name, result=result, changes=build.changes,
1374 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001375 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001376 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001377 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001378 )
Paul Belanger174a8272017-03-14 13:20:10 -04001379 self.executor_server.running_builds.remove(build)
1380 del self.executor_server.job_builds[self.job.unique]
1381 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001382
1383 def runPlaybooks(self, args):
1384 build = self.executor_server.job_builds[self.job.unique]
1385 build.jobdir = self.jobdir
1386
1387 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1388 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001389 return result
1390
James E. Blair74a82cf2017-07-12 17:23:08 -07001391 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001392 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001393
Paul Belanger174a8272017-03-14 13:20:10 -04001394 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001395 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001396 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001397 else:
1398 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001399 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001400
James E. Blairad8dca02017-02-21 11:48:32 -05001401 def getHostList(self, args):
1402 self.log.debug("hostlist")
1403 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001404 for host in hosts:
1405 host['host_vars']['ansible_connection'] = 'local'
1406
1407 hosts.append(dict(
1408 name='localhost',
1409 host_vars=dict(ansible_connection='local'),
1410 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001411 return hosts
1412
James E. Blairf5dbd002015-12-23 15:26:17 -08001413
Clark Boylanb640e052014-04-03 16:41:46 -07001414class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001415 """A Gearman server for use in tests.
1416
1417 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1418 added to the queue but will not be distributed to workers
1419 until released. This attribute may be changed at any time and
1420 will take effect for subsequently enqueued jobs, but
1421 previously held jobs will still need to be explicitly
1422 released.
1423
1424 """
1425
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001426 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001427 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001428 if use_ssl:
1429 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1430 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1431 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1432 else:
1433 ssl_ca = None
1434 ssl_cert = None
1435 ssl_key = None
1436
1437 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1438 ssl_cert=ssl_cert,
1439 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001440
1441 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001442 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1443 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001444 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001445 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001446 job.waiting = self.hold_jobs_in_queue
1447 else:
1448 job.waiting = False
1449 if job.waiting:
1450 continue
1451 if job.name in connection.functions:
1452 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001453 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001454 connection.related_jobs[job.handle] = job
1455 job.worker_connection = connection
1456 job.running = True
1457 return job
1458 return None
1459
1460 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001461 """Release a held job.
1462
1463 :arg str regex: A regular expression which, if supplied, will
1464 cause only jobs with matching names to be released. If
1465 not supplied, all jobs will be released.
1466 """
Clark Boylanb640e052014-04-03 16:41:46 -07001467 released = False
1468 qlen = (len(self.high_queue) + len(self.normal_queue) +
1469 len(self.low_queue))
1470 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1471 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001472 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001473 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001474 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001475 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001476 self.log.debug("releasing queued job %s" %
1477 job.unique)
1478 job.waiting = False
1479 released = True
1480 else:
1481 self.log.debug("not releasing queued job %s" %
1482 job.unique)
1483 if released:
1484 self.wakeConnections()
1485 qlen = (len(self.high_queue) + len(self.normal_queue) +
1486 len(self.low_queue))
1487 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1488
1489
1490class FakeSMTP(object):
1491 log = logging.getLogger('zuul.FakeSMTP')
1492
1493 def __init__(self, messages, server, port):
1494 self.server = server
1495 self.port = port
1496 self.messages = messages
1497
1498 def sendmail(self, from_email, to_email, msg):
1499 self.log.info("Sending email from %s, to %s, with msg %s" % (
1500 from_email, to_email, msg))
1501
1502 headers = msg.split('\n\n', 1)[0]
1503 body = msg.split('\n\n', 1)[1]
1504
1505 self.messages.append(dict(
1506 from_email=from_email,
1507 to_email=to_email,
1508 msg=msg,
1509 headers=headers,
1510 body=body,
1511 ))
1512
1513 return True
1514
1515 def quit(self):
1516 return True
1517
1518
James E. Blairdce6cea2016-12-20 16:45:32 -08001519class FakeNodepool(object):
1520 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001521 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001522
1523 log = logging.getLogger("zuul.test.FakeNodepool")
1524
1525 def __init__(self, host, port, chroot):
1526 self.client = kazoo.client.KazooClient(
1527 hosts='%s:%s%s' % (host, port, chroot))
1528 self.client.start()
1529 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001530 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001531 self.thread = threading.Thread(target=self.run)
1532 self.thread.daemon = True
1533 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001534 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001535
1536 def stop(self):
1537 self._running = False
1538 self.thread.join()
1539 self.client.stop()
1540 self.client.close()
1541
1542 def run(self):
1543 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001544 try:
1545 self._run()
1546 except Exception:
1547 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001548 time.sleep(0.1)
1549
1550 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001551 if self.paused:
1552 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001553 for req in self.getNodeRequests():
1554 self.fulfillRequest(req)
1555
1556 def getNodeRequests(self):
1557 try:
1558 reqids = self.client.get_children(self.REQUEST_ROOT)
1559 except kazoo.exceptions.NoNodeError:
1560 return []
1561 reqs = []
1562 for oid in sorted(reqids):
1563 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001564 try:
1565 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001566 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001567 data['_oid'] = oid
1568 reqs.append(data)
1569 except kazoo.exceptions.NoNodeError:
1570 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001571 return reqs
1572
James E. Blaire18d4602017-01-05 11:17:28 -08001573 def getNodes(self):
1574 try:
1575 nodeids = self.client.get_children(self.NODE_ROOT)
1576 except kazoo.exceptions.NoNodeError:
1577 return []
1578 nodes = []
1579 for oid in sorted(nodeids):
1580 path = self.NODE_ROOT + '/' + oid
1581 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001582 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001583 data['_oid'] = oid
1584 try:
1585 lockfiles = self.client.get_children(path + '/lock')
1586 except kazoo.exceptions.NoNodeError:
1587 lockfiles = []
1588 if lockfiles:
1589 data['_lock'] = True
1590 else:
1591 data['_lock'] = False
1592 nodes.append(data)
1593 return nodes
1594
James E. Blaira38c28e2017-01-04 10:33:20 -08001595 def makeNode(self, request_id, node_type):
1596 now = time.time()
1597 path = '/nodepool/nodes/'
1598 data = dict(type=node_type,
1599 provider='test-provider',
1600 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001601 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001602 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001603 public_ipv4='127.0.0.1',
1604 private_ipv4=None,
1605 public_ipv6=None,
1606 allocated_to=request_id,
1607 state='ready',
1608 state_time=now,
1609 created_time=now,
1610 updated_time=now,
1611 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001612 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001613 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001614 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001615 path = self.client.create(path, data,
1616 makepath=True,
1617 sequence=True)
1618 nodeid = path.split("/")[-1]
1619 return nodeid
1620
James E. Blair6ab79e02017-01-06 10:10:17 -08001621 def addFailRequest(self, request):
1622 self.fail_requests.add(request['_oid'])
1623
James E. Blairdce6cea2016-12-20 16:45:32 -08001624 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001625 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001626 return
1627 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001628 oid = request['_oid']
1629 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001630
James E. Blair6ab79e02017-01-06 10:10:17 -08001631 if oid in self.fail_requests:
1632 request['state'] = 'failed'
1633 else:
1634 request['state'] = 'fulfilled'
1635 nodes = []
1636 for node in request['node_types']:
1637 nodeid = self.makeNode(oid, node)
1638 nodes.append(nodeid)
1639 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001640
James E. Blaira38c28e2017-01-04 10:33:20 -08001641 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001642 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001643 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001644 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001645 try:
1646 self.client.set(path, data)
1647 except kazoo.exceptions.NoNodeError:
1648 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001649
1650
James E. Blair498059b2016-12-20 13:50:13 -08001651class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001652 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001653 super(ChrootedKazooFixture, self).__init__()
1654
1655 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1656 if ':' in zk_host:
1657 host, port = zk_host.split(':')
1658 else:
1659 host = zk_host
1660 port = None
1661
1662 self.zookeeper_host = host
1663
1664 if not port:
1665 self.zookeeper_port = 2181
1666 else:
1667 self.zookeeper_port = int(port)
1668
Clark Boylan621ec9a2017-04-07 17:41:33 -07001669 self.test_id = test_id
1670
James E. Blair498059b2016-12-20 13:50:13 -08001671 def _setUp(self):
1672 # Make sure the test chroot paths do not conflict
1673 random_bits = ''.join(random.choice(string.ascii_lowercase +
1674 string.ascii_uppercase)
1675 for x in range(8))
1676
Clark Boylan621ec9a2017-04-07 17:41:33 -07001677 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001678 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1679
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001680 self.addCleanup(self._cleanup)
1681
James E. Blair498059b2016-12-20 13:50:13 -08001682 # Ensure the chroot path exists and clean up any pre-existing znodes.
1683 _tmp_client = kazoo.client.KazooClient(
1684 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1685 _tmp_client.start()
1686
1687 if _tmp_client.exists(self.zookeeper_chroot):
1688 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1689
1690 _tmp_client.ensure_path(self.zookeeper_chroot)
1691 _tmp_client.stop()
1692 _tmp_client.close()
1693
James E. Blair498059b2016-12-20 13:50:13 -08001694 def _cleanup(self):
1695 '''Remove the chroot path.'''
1696 # Need a non-chroot'ed client to remove the chroot path
1697 _tmp_client = kazoo.client.KazooClient(
1698 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1699 _tmp_client.start()
1700 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1701 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001702 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001703
1704
Joshua Heskethd78b4482015-09-14 16:56:34 -06001705class MySQLSchemaFixture(fixtures.Fixture):
1706 def setUp(self):
1707 super(MySQLSchemaFixture, self).setUp()
1708
1709 random_bits = ''.join(random.choice(string.ascii_lowercase +
1710 string.ascii_uppercase)
1711 for x in range(8))
1712 self.name = '%s_%s' % (random_bits, os.getpid())
1713 self.passwd = uuid.uuid4().hex
1714 db = pymysql.connect(host="localhost",
1715 user="openstack_citest",
1716 passwd="openstack_citest",
1717 db="openstack_citest")
1718 cur = db.cursor()
1719 cur.execute("create database %s" % self.name)
1720 cur.execute(
1721 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1722 (self.name, self.name, self.passwd))
1723 cur.execute("flush privileges")
1724
1725 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1726 self.passwd,
1727 self.name)
1728 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1729 self.addCleanup(self.cleanup)
1730
1731 def cleanup(self):
1732 db = pymysql.connect(host="localhost",
1733 user="openstack_citest",
1734 passwd="openstack_citest",
1735 db="openstack_citest")
1736 cur = db.cursor()
1737 cur.execute("drop database %s" % self.name)
1738 cur.execute("drop user '%s'@'localhost'" % self.name)
1739 cur.execute("flush privileges")
1740
1741
Maru Newby3fe5f852015-01-13 04:22:14 +00001742class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001743 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001744 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001745
James E. Blair1c236df2017-02-01 14:07:24 -08001746 def attachLogs(self, *args):
1747 def reader():
1748 self._log_stream.seek(0)
1749 while True:
1750 x = self._log_stream.read(4096)
1751 if not x:
1752 break
1753 yield x.encode('utf8')
1754 content = testtools.content.content_from_reader(
1755 reader,
1756 testtools.content_type.UTF8_TEXT,
1757 False)
1758 self.addDetail('logging', content)
1759
Clark Boylanb640e052014-04-03 16:41:46 -07001760 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001761 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001762 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1763 try:
1764 test_timeout = int(test_timeout)
1765 except ValueError:
1766 # If timeout value is invalid do not set a timeout.
1767 test_timeout = 0
1768 if test_timeout > 0:
1769 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1770
1771 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1772 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1773 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1774 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1775 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1776 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1777 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1778 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1779 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1780 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001781 self._log_stream = StringIO()
1782 self.addOnException(self.attachLogs)
1783 else:
1784 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001785
James E. Blair73b41772017-05-22 13:22:55 -07001786 # NOTE(jeblair): this is temporary extra debugging to try to
1787 # track down a possible leak.
1788 orig_git_repo_init = git.Repo.__init__
1789
1790 def git_repo_init(myself, *args, **kw):
1791 orig_git_repo_init(myself, *args, **kw)
1792 self.log.debug("Created git repo 0x%x %s" %
1793 (id(myself), repr(myself)))
1794
1795 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1796 git_repo_init))
1797
James E. Blair1c236df2017-02-01 14:07:24 -08001798 handler = logging.StreamHandler(self._log_stream)
1799 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1800 '%(levelname)-8s %(message)s')
1801 handler.setFormatter(formatter)
1802
1803 logger = logging.getLogger()
1804 logger.setLevel(logging.DEBUG)
1805 logger.addHandler(handler)
1806
Clark Boylan3410d532017-04-25 12:35:29 -07001807 # Make sure we don't carry old handlers around in process state
1808 # which slows down test runs
1809 self.addCleanup(logger.removeHandler, handler)
1810 self.addCleanup(handler.close)
1811 self.addCleanup(handler.flush)
1812
James E. Blair1c236df2017-02-01 14:07:24 -08001813 # NOTE(notmorgan): Extract logging overrides for specific
1814 # libraries from the OS_LOG_DEFAULTS env and create loggers
1815 # for each. This is used to limit the output during test runs
1816 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001817 log_defaults_from_env = os.environ.get(
1818 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001819 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001820
James E. Blairdce6cea2016-12-20 16:45:32 -08001821 if log_defaults_from_env:
1822 for default in log_defaults_from_env.split(','):
1823 try:
1824 name, level_str = default.split('=', 1)
1825 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001826 logger = logging.getLogger(name)
1827 logger.setLevel(level)
1828 logger.addHandler(handler)
1829 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001830 except ValueError:
1831 # NOTE(notmorgan): Invalid format of the log default,
1832 # skip and don't try and apply a logger for the
1833 # specified module
1834 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001835
Maru Newby3fe5f852015-01-13 04:22:14 +00001836
1837class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001838 """A test case with a functioning Zuul.
1839
1840 The following class variables are used during test setup and can
1841 be overidden by subclasses but are effectively read-only once a
1842 test method starts running:
1843
1844 :cvar str config_file: This points to the main zuul config file
1845 within the fixtures directory. Subclasses may override this
1846 to obtain a different behavior.
1847
1848 :cvar str tenant_config_file: This is the tenant config file
1849 (which specifies from what git repos the configuration should
1850 be loaded). It defaults to the value specified in
1851 `config_file` but can be overidden by subclasses to obtain a
1852 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001853 configuration. See also the :py:func:`simple_layout`
1854 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001855
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001856 :cvar bool create_project_keys: Indicates whether Zuul should
1857 auto-generate keys for each project, or whether the test
1858 infrastructure should insert dummy keys to save time during
1859 startup. Defaults to False.
1860
James E. Blaire7b99a02016-08-05 14:27:34 -07001861 The following are instance variables that are useful within test
1862 methods:
1863
1864 :ivar FakeGerritConnection fake_<connection>:
1865 A :py:class:`~tests.base.FakeGerritConnection` will be
1866 instantiated for each connection present in the config file
1867 and stored here. For instance, `fake_gerrit` will hold the
1868 FakeGerritConnection object for a connection named `gerrit`.
1869
1870 :ivar FakeGearmanServer gearman_server: An instance of
1871 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1872 server that all of the Zuul components in this test use to
1873 communicate with each other.
1874
Paul Belanger174a8272017-03-14 13:20:10 -04001875 :ivar RecordingExecutorServer executor_server: An instance of
1876 :py:class:`~tests.base.RecordingExecutorServer` which is the
1877 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001878
1879 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1880 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001881 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001882 list upon completion.
1883
1884 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1885 objects representing completed builds. They are appended to
1886 the list in the order they complete.
1887
1888 """
1889
James E. Blair83005782015-12-11 14:46:03 -08001890 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001891 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001892 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001893 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001894
1895 def _startMerger(self):
1896 self.merge_server = zuul.merger.server.MergeServer(self.config,
1897 self.connections)
1898 self.merge_server.start()
1899
Maru Newby3fe5f852015-01-13 04:22:14 +00001900 def setUp(self):
1901 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001902
1903 self.setupZK()
1904
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001905 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001906 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001907 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1908 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001909 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001910 tmp_root = tempfile.mkdtemp(
1911 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001912 self.test_root = os.path.join(tmp_root, "zuul-test")
1913 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001914 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001915 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001916 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001917 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1918 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001919
1920 if os.path.exists(self.test_root):
1921 shutil.rmtree(self.test_root)
1922 os.makedirs(self.test_root)
1923 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001924 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001925 os.makedirs(self.merger_state_root)
1926 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001927
1928 # Make per test copy of Configuration.
1929 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001930 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1931 if not os.path.exists(self.private_key_file):
1932 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1933 shutil.copy(src_private_key_file, self.private_key_file)
1934 shutil.copy('{}.pub'.format(src_private_key_file),
1935 '{}.pub'.format(self.private_key_file))
1936 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001937 self.config.set('scheduler', 'tenant_config',
1938 os.path.join(
1939 FIXTURE_DIR,
1940 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001941 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05001942 self.config.set('merger', 'git_dir', self.merger_src_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001943 self.config.set('merger', 'state_dir', self.merger_state_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001944 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001945 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001946 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001947
Clark Boylanb640e052014-04-03 16:41:46 -07001948 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001949 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1950 # see: https://github.com/jsocol/pystatsd/issues/61
1951 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001952 os.environ['STATSD_PORT'] = str(self.statsd.port)
1953 self.statsd.start()
1954 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001955 importlib.reload(statsd)
1956 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001957
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001958 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001959
1960 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001961 self.log.info("Gearman server on port %s" %
1962 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001963 if self.use_ssl:
1964 self.log.info('SSL enabled for gearman')
1965 self.config.set(
1966 'gearman', 'ssl_ca',
1967 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1968 self.config.set(
1969 'gearman', 'ssl_cert',
1970 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1971 self.config.set(
1972 'gearman', 'ssl_key',
1973 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001974
James E. Blaire511d2f2016-12-08 15:22:26 -08001975 gerritsource.GerritSource.replication_timeout = 1.5
1976 gerritsource.GerritSource.replication_retry_interval = 0.5
1977 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001978
Joshua Hesketh352264b2015-08-11 23:42:08 +10001979 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001980
Jan Hruban7083edd2015-08-21 14:00:54 +02001981 self.webapp = zuul.webapp.WebApp(
1982 self.sched, port=0, listen_address='127.0.0.1')
1983
Jan Hruban6b71aff2015-10-22 16:58:08 +02001984 self.event_queues = [
1985 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001986 self.sched.trigger_event_queue,
1987 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001988 ]
1989
James E. Blairfef78942016-03-11 16:28:56 -08001990 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001991 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001992
Paul Belanger174a8272017-03-14 13:20:10 -04001993 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001994 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001995 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001996 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001997 _test_root=self.test_root,
1998 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001999 self.executor_server.start()
2000 self.history = self.executor_server.build_history
2001 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002002
Paul Belanger174a8272017-03-14 13:20:10 -04002003 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002004 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002005 self.merge_client = zuul.merger.client.MergeClient(
2006 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002007 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002008 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002009 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002010
James E. Blair0d5a36e2017-02-21 10:53:44 -05002011 self.fake_nodepool = FakeNodepool(
2012 self.zk_chroot_fixture.zookeeper_host,
2013 self.zk_chroot_fixture.zookeeper_port,
2014 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002015
Paul Belanger174a8272017-03-14 13:20:10 -04002016 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002017 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002018 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002019 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002020
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002021 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002022
2023 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002024 self.webapp.start()
2025 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002026 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002027 # Cleanups are run in reverse order
2028 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002029 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002030 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002031
James E. Blairb9c0d772017-03-03 14:34:49 -08002032 self.sched.reconfigure(self.config)
2033 self.sched.resume()
2034
Tobias Henkel7df274b2017-05-26 17:41:11 +02002035 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002036 # Set up gerrit related fakes
2037 # Set a changes database so multiple FakeGerrit's can report back to
2038 # a virtual canonical database given by the configured hostname
2039 self.gerrit_changes_dbs = {}
2040
2041 def getGerritConnection(driver, name, config):
2042 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2043 con = FakeGerritConnection(driver, name, config,
2044 changes_db=db,
2045 upstream_root=self.upstream_root)
2046 self.event_queues.append(con.event_queue)
2047 setattr(self, 'fake_' + name, con)
2048 return con
2049
2050 self.useFixture(fixtures.MonkeyPatch(
2051 'zuul.driver.gerrit.GerritDriver.getConnection',
2052 getGerritConnection))
2053
Gregory Haynes4fc12542015-04-22 20:38:06 -07002054 def getGithubConnection(driver, name, config):
2055 con = FakeGithubConnection(driver, name, config,
2056 upstream_root=self.upstream_root)
2057 setattr(self, 'fake_' + name, con)
2058 return con
2059
2060 self.useFixture(fixtures.MonkeyPatch(
2061 'zuul.driver.github.GithubDriver.getConnection',
2062 getGithubConnection))
2063
James E. Blaire511d2f2016-12-08 15:22:26 -08002064 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002065 # TODO(jhesketh): This should come from lib.connections for better
2066 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002067 # Register connections from the config
2068 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002069
Joshua Hesketh352264b2015-08-11 23:42:08 +10002070 def FakeSMTPFactory(*args, **kw):
2071 args = [self.smtp_messages] + list(args)
2072 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002073
Joshua Hesketh352264b2015-08-11 23:42:08 +10002074 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002075
James E. Blaire511d2f2016-12-08 15:22:26 -08002076 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002077 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002078 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002079
James E. Blair83005782015-12-11 14:46:03 -08002080 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002081 # This creates the per-test configuration object. It can be
2082 # overriden by subclasses, but should not need to be since it
2083 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002084 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002085 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002086
James E. Blair39840362017-06-23 20:34:02 +01002087 sections = ['zuul', 'scheduler', 'executor', 'merger']
2088 for section in sections:
2089 if not self.config.has_section(section):
2090 self.config.add_section(section)
2091
James E. Blair06cc3922017-04-19 10:08:10 -07002092 if not self.setupSimpleLayout():
2093 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002094 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002095 self.tenant_config_file)
2096 git_path = os.path.join(
2097 os.path.dirname(
2098 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2099 'git')
2100 if os.path.exists(git_path):
2101 for reponame in os.listdir(git_path):
2102 project = reponame.replace('_', '/')
2103 self.copyDirToRepo(project,
2104 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002105 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002106 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002107 self.setupAllProjectKeys()
2108
James E. Blair06cc3922017-04-19 10:08:10 -07002109 def setupSimpleLayout(self):
2110 # If the test method has been decorated with a simple_layout,
2111 # use that instead of the class tenant_config_file. Set up a
2112 # single config-project with the specified layout, and
2113 # initialize repos for all of the 'project' entries which
2114 # appear in the layout.
2115 test_name = self.id().split('.')[-1]
2116 test = getattr(self, test_name)
2117 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002118 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002119 else:
2120 return False
2121
James E. Blairb70e55a2017-04-19 12:57:02 -07002122 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002123 path = os.path.join(FIXTURE_DIR, path)
2124 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002125 data = f.read()
2126 layout = yaml.safe_load(data)
2127 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002128 untrusted_projects = []
2129 for item in layout:
2130 if 'project' in item:
2131 name = item['project']['name']
2132 untrusted_projects.append(name)
2133 self.init_repo(name)
2134 self.addCommitToRepo(name, 'initial commit',
2135 files={'README': ''},
2136 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002137 if 'job' in item:
2138 jobname = item['job']['name']
2139 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002140
2141 root = os.path.join(self.test_root, "config")
2142 if not os.path.exists(root):
2143 os.makedirs(root)
2144 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2145 config = [{'tenant':
2146 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002147 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002148 {'config-projects': ['common-config'],
2149 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002150 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002151 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002152 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002153 os.path.join(FIXTURE_DIR, f.name))
2154
2155 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002156 self.addCommitToRepo('common-config', 'add content from fixture',
2157 files, branch='master', tag='init')
2158
2159 return True
2160
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002161 def setupAllProjectKeys(self):
2162 if self.create_project_keys:
2163 return
2164
James E. Blair39840362017-06-23 20:34:02 +01002165 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002166 with open(os.path.join(FIXTURE_DIR, path)) as f:
2167 tenant_config = yaml.safe_load(f.read())
2168 for tenant in tenant_config:
2169 sources = tenant['tenant']['source']
2170 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002171 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002172 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002173 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002174 self.setupProjectKeys(source, project)
2175
2176 def setupProjectKeys(self, source, project):
2177 # Make sure we set up an RSA key for the project so that we
2178 # don't spend time generating one:
2179
James E. Blair6459db12017-06-29 14:57:20 -07002180 if isinstance(project, dict):
2181 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002182 key_root = os.path.join(self.state_root, 'keys')
2183 if not os.path.isdir(key_root):
2184 os.mkdir(key_root, 0o700)
2185 private_key_file = os.path.join(key_root, source, project + '.pem')
2186 private_key_dir = os.path.dirname(private_key_file)
2187 self.log.debug("Installing test keys for project %s at %s" % (
2188 project, private_key_file))
2189 if not os.path.isdir(private_key_dir):
2190 os.makedirs(private_key_dir)
2191 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2192 with open(private_key_file, 'w') as o:
2193 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002194
James E. Blair498059b2016-12-20 13:50:13 -08002195 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002196 self.zk_chroot_fixture = self.useFixture(
2197 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002198 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002199 self.zk_chroot_fixture.zookeeper_host,
2200 self.zk_chroot_fixture.zookeeper_port,
2201 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002202
James E. Blair96c6bf82016-01-15 16:20:40 -08002203 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002204 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002205
2206 files = {}
2207 for (dirpath, dirnames, filenames) in os.walk(source_path):
2208 for filename in filenames:
2209 test_tree_filepath = os.path.join(dirpath, filename)
2210 common_path = os.path.commonprefix([test_tree_filepath,
2211 source_path])
2212 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2213 with open(test_tree_filepath, 'r') as f:
2214 content = f.read()
2215 files[relative_filepath] = content
2216 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002217 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002218
James E. Blaire18d4602017-01-05 11:17:28 -08002219 def assertNodepoolState(self):
2220 # Make sure that there are no pending requests
2221
2222 requests = self.fake_nodepool.getNodeRequests()
2223 self.assertEqual(len(requests), 0)
2224
2225 nodes = self.fake_nodepool.getNodes()
2226 for node in nodes:
2227 self.assertFalse(node['_lock'], "Node %s is locked" %
2228 (node['_oid'],))
2229
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002230 def assertNoGeneratedKeys(self):
2231 # Make sure that Zuul did not generate any project keys
2232 # (unless it was supposed to).
2233
2234 if self.create_project_keys:
2235 return
2236
2237 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2238 test_key = i.read()
2239
2240 key_root = os.path.join(self.state_root, 'keys')
2241 for root, dirname, files in os.walk(key_root):
2242 for fn in files:
2243 with open(os.path.join(root, fn)) as f:
2244 self.assertEqual(test_key, f.read())
2245
Clark Boylanb640e052014-04-03 16:41:46 -07002246 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002247 self.log.debug("Assert final state")
2248 # Make sure no jobs are running
2249 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002250 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002251 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002252 gc.collect()
2253 for obj in gc.get_objects():
2254 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002255 self.log.debug("Leaked git repo object: 0x%x %s" %
2256 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002257 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002258 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002259 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002260 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002261 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002262 for tenant in self.sched.abide.tenants.values():
2263 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002264 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002265 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002266
2267 def shutdown(self):
2268 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002269 self.executor_server.hold_jobs_in_build = False
2270 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002271 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002272 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002273 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002274 self.sched.stop()
2275 self.sched.join()
2276 self.statsd.stop()
2277 self.statsd.join()
2278 self.webapp.stop()
2279 self.webapp.join()
2280 self.rpc.stop()
2281 self.rpc.join()
2282 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002283 self.fake_nodepool.stop()
2284 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002285 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002286 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002287 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002288 # Further the pydevd threads also need to be whitelisted so debugging
2289 # e.g. in PyCharm is possible without breaking shutdown.
2290 whitelist = ['executor-watchdog',
2291 'pydevd.CommandThread',
2292 'pydevd.Reader',
2293 'pydevd.Writer',
2294 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002295 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002296 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002297 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002298 log_str = ""
2299 for thread_id, stack_frame in sys._current_frames().items():
2300 log_str += "Thread: %s\n" % thread_id
2301 log_str += "".join(traceback.format_stack(stack_frame))
2302 self.log.debug(log_str)
2303 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002304
James E. Blaira002b032017-04-18 10:35:48 -07002305 def assertCleanShutdown(self):
2306 pass
2307
James E. Blairc4ba97a2017-04-19 16:26:24 -07002308 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002309 parts = project.split('/')
2310 path = os.path.join(self.upstream_root, *parts[:-1])
2311 if not os.path.exists(path):
2312 os.makedirs(path)
2313 path = os.path.join(self.upstream_root, project)
2314 repo = git.Repo.init(path)
2315
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002316 with repo.config_writer() as config_writer:
2317 config_writer.set_value('user', 'email', 'user@example.com')
2318 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002319
Clark Boylanb640e052014-04-03 16:41:46 -07002320 repo.index.commit('initial commit')
2321 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002322 if tag:
2323 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002324
James E. Blair97d902e2014-08-21 13:25:56 -07002325 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002326 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002327 repo.git.clean('-x', '-f', '-d')
2328
James E. Blair97d902e2014-08-21 13:25:56 -07002329 def create_branch(self, project, branch):
2330 path = os.path.join(self.upstream_root, project)
2331 repo = git.Repo.init(path)
2332 fn = os.path.join(path, 'README')
2333
2334 branch_head = repo.create_head(branch)
2335 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002336 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002337 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002338 f.close()
2339 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002340 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002341
James E. Blair97d902e2014-08-21 13:25:56 -07002342 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002343 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002344 repo.git.clean('-x', '-f', '-d')
2345
Sachi King9f16d522016-03-16 12:20:45 +11002346 def create_commit(self, project):
2347 path = os.path.join(self.upstream_root, project)
2348 repo = git.Repo(path)
2349 repo.head.reference = repo.heads['master']
2350 file_name = os.path.join(path, 'README')
2351 with open(file_name, 'a') as f:
2352 f.write('creating fake commit\n')
2353 repo.index.add([file_name])
2354 commit = repo.index.commit('Creating a fake commit')
2355 return commit.hexsha
2356
James E. Blairf4a5f022017-04-18 14:01:10 -07002357 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002358 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002359 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002360 while len(self.builds):
2361 self.release(self.builds[0])
2362 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002363 i += 1
2364 if count is not None and i >= count:
2365 break
James E. Blairb8c16472015-05-05 14:55:26 -07002366
James E. Blairdf25ddc2017-07-08 07:57:09 -07002367 def getSortedBuilds(self):
2368 "Return the list of currently running builds sorted by name"
2369
2370 return sorted(self.builds, key=lambda x: x.name)
2371
Clark Boylanb640e052014-04-03 16:41:46 -07002372 def release(self, job):
2373 if isinstance(job, FakeBuild):
2374 job.release()
2375 else:
2376 job.waiting = False
2377 self.log.debug("Queued job %s released" % job.unique)
2378 self.gearman_server.wakeConnections()
2379
2380 def getParameter(self, job, name):
2381 if isinstance(job, FakeBuild):
2382 return job.parameters[name]
2383 else:
2384 parameters = json.loads(job.arguments)
2385 return parameters[name]
2386
Clark Boylanb640e052014-04-03 16:41:46 -07002387 def haveAllBuildsReported(self):
2388 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002389 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002390 return False
2391 # Find out if every build that the worker has completed has been
2392 # reported back to Zuul. If it hasn't then that means a Gearman
2393 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002394 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002395 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002396 if not zbuild:
2397 # It has already been reported
2398 continue
2399 # It hasn't been reported yet.
2400 return False
2401 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002402 worker = self.executor_server.executor_worker
2403 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002404 if connection.state == 'GRAB_WAIT':
2405 return False
2406 return True
2407
2408 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002409 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002410 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002411 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002412 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002413 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002414 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002415 for j in conn.related_jobs.values():
2416 if j.unique == build.uuid:
2417 client_job = j
2418 break
2419 if not client_job:
2420 self.log.debug("%s is not known to the gearman client" %
2421 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002422 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002423 if not client_job.handle:
2424 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002425 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002426 server_job = self.gearman_server.jobs.get(client_job.handle)
2427 if not server_job:
2428 self.log.debug("%s is not known to the gearman server" %
2429 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002430 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002431 if not hasattr(server_job, 'waiting'):
2432 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002433 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002434 if server_job.waiting:
2435 continue
James E. Blair17302972016-08-10 16:11:42 -07002436 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002437 self.log.debug("%s has not reported start" % build)
2438 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002439 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002440 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002441 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002442 if worker_build:
2443 if worker_build.isWaiting():
2444 continue
2445 else:
2446 self.log.debug("%s is running" % worker_build)
2447 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002448 else:
James E. Blair962220f2016-08-03 11:22:38 -07002449 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002450 return False
James E. Blaira002b032017-04-18 10:35:48 -07002451 for (build_uuid, job_worker) in \
2452 self.executor_server.job_workers.items():
2453 if build_uuid not in seen_builds:
2454 self.log.debug("%s is not finalized" % build_uuid)
2455 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002456 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002457
James E. Blairdce6cea2016-12-20 16:45:32 -08002458 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002459 if self.fake_nodepool.paused:
2460 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002461 if self.sched.nodepool.requests:
2462 return False
2463 return True
2464
Jan Hruban6b71aff2015-10-22 16:58:08 +02002465 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002466 for event_queue in self.event_queues:
2467 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002468
2469 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002470 for event_queue in self.event_queues:
2471 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002472
Clark Boylanb640e052014-04-03 16:41:46 -07002473 def waitUntilSettled(self):
2474 self.log.debug("Waiting until settled...")
2475 start = time.time()
2476 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002477 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002478 self.log.error("Timeout waiting for Zuul to settle")
2479 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002480 for event_queue in self.event_queues:
2481 self.log.error(" %s: %s" %
2482 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002483 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002484 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002485 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002486 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002487 self.log.error("All requests completed: %s" %
2488 (self.areAllNodeRequestsComplete(),))
2489 self.log.error("Merge client jobs: %s" %
2490 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002491 raise Exception("Timeout waiting for Zuul to settle")
2492 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002493
Paul Belanger174a8272017-03-14 13:20:10 -04002494 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002495 # have all build states propogated to zuul?
2496 if self.haveAllBuildsReported():
2497 # Join ensures that the queue is empty _and_ events have been
2498 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002499 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002500 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002501 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002502 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002503 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002504 self.areAllNodeRequestsComplete() and
2505 all(self.eventQueuesEmpty())):
2506 # The queue empty check is placed at the end to
2507 # ensure that if a component adds an event between
2508 # when locked the run handler and checked that the
2509 # components were stable, we don't erroneously
2510 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002511 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002512 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002513 self.log.debug("...settled.")
2514 return
2515 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002516 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002517 self.sched.wake_event.wait(0.1)
2518
2519 def countJobResults(self, jobs, result):
2520 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002521 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002522
Monty Taylor0d926122017-05-24 08:07:56 -05002523 def getBuildByName(self, name):
2524 for build in self.builds:
2525 if build.name == name:
2526 return build
2527 raise Exception("Unable to find build %s" % name)
2528
James E. Blair96c6bf82016-01-15 16:20:40 -08002529 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002530 for job in self.history:
2531 if (job.name == name and
2532 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002533 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002534 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002535 raise Exception("Unable to find job %s in history" % name)
2536
2537 def assertEmptyQueues(self):
2538 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002539 for tenant in self.sched.abide.tenants.values():
2540 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002541 for pipeline_queue in pipeline.queues:
2542 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002543 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002544 pipeline.name, pipeline_queue.name,
2545 pipeline_queue.queue))
2546 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002547 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002548
2549 def assertReportedStat(self, key, value=None, kind=None):
2550 start = time.time()
2551 while time.time() < (start + 5):
2552 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002553 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002554 if key == k:
2555 if value is None and kind is None:
2556 return
2557 elif value:
2558 if value == v:
2559 return
2560 elif kind:
2561 if v.endswith('|' + kind):
2562 return
2563 time.sleep(0.1)
2564
Clark Boylanb640e052014-04-03 16:41:46 -07002565 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002566
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002567 def assertBuilds(self, builds):
2568 """Assert that the running builds are as described.
2569
2570 The list of running builds is examined and must match exactly
2571 the list of builds described by the input.
2572
2573 :arg list builds: A list of dictionaries. Each item in the
2574 list must match the corresponding build in the build
2575 history, and each element of the dictionary must match the
2576 corresponding attribute of the build.
2577
2578 """
James E. Blair3158e282016-08-19 09:34:11 -07002579 try:
2580 self.assertEqual(len(self.builds), len(builds))
2581 for i, d in enumerate(builds):
2582 for k, v in d.items():
2583 self.assertEqual(
2584 getattr(self.builds[i], k), v,
2585 "Element %i in builds does not match" % (i,))
2586 except Exception:
2587 for build in self.builds:
2588 self.log.error("Running build: %s" % build)
2589 else:
2590 self.log.error("No running builds")
2591 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002592
James E. Blairb536ecc2016-08-31 10:11:42 -07002593 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002594 """Assert that the completed builds are as described.
2595
2596 The list of completed builds is examined and must match
2597 exactly the list of builds described by the input.
2598
2599 :arg list history: A list of dictionaries. Each item in the
2600 list must match the corresponding build in the build
2601 history, and each element of the dictionary must match the
2602 corresponding attribute of the build.
2603
James E. Blairb536ecc2016-08-31 10:11:42 -07002604 :arg bool ordered: If true, the history must match the order
2605 supplied, if false, the builds are permitted to have
2606 arrived in any order.
2607
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002608 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002609 def matches(history_item, item):
2610 for k, v in item.items():
2611 if getattr(history_item, k) != v:
2612 return False
2613 return True
James E. Blair3158e282016-08-19 09:34:11 -07002614 try:
2615 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002616 if ordered:
2617 for i, d in enumerate(history):
2618 if not matches(self.history[i], d):
2619 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002620 "Element %i in history does not match %s" %
2621 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002622 else:
2623 unseen = self.history[:]
2624 for i, d in enumerate(history):
2625 found = False
2626 for unseen_item in unseen:
2627 if matches(unseen_item, d):
2628 found = True
2629 unseen.remove(unseen_item)
2630 break
2631 if not found:
2632 raise Exception("No match found for element %i "
2633 "in history" % (i,))
2634 if unseen:
2635 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002636 except Exception:
2637 for build in self.history:
2638 self.log.error("Completed build: %s" % build)
2639 else:
2640 self.log.error("No completed builds")
2641 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002642
James E. Blair6ac368c2016-12-22 18:07:20 -08002643 def printHistory(self):
2644 """Log the build history.
2645
2646 This can be useful during tests to summarize what jobs have
2647 completed.
2648
2649 """
2650 self.log.debug("Build history:")
2651 for build in self.history:
2652 self.log.debug(build)
2653
James E. Blair59fdbac2015-12-07 17:08:06 -08002654 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002655 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2656
James E. Blair9ea70072017-04-19 16:05:30 -07002657 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002658 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002659 if not os.path.exists(root):
2660 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002661 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2662 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002663- tenant:
2664 name: openstack
2665 source:
2666 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002667 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002668 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002669 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002670 - org/project
2671 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002672 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002673 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002674 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002675 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002676 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002677
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002678 def addCommitToRepo(self, project, message, files,
2679 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002680 path = os.path.join(self.upstream_root, project)
2681 repo = git.Repo(path)
2682 repo.head.reference = branch
2683 zuul.merger.merger.reset_repo_to_head(repo)
2684 for fn, content in files.items():
2685 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002686 try:
2687 os.makedirs(os.path.dirname(fn))
2688 except OSError:
2689 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002690 with open(fn, 'w') as f:
2691 f.write(content)
2692 repo.index.add([fn])
2693 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002694 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002695 repo.heads[branch].commit = commit
2696 repo.head.reference = branch
2697 repo.git.clean('-x', '-f', '-d')
2698 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002699 if tag:
2700 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002701 return before
2702
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002703 def commitConfigUpdate(self, project_name, source_name):
2704 """Commit an update to zuul.yaml
2705
2706 This overwrites the zuul.yaml in the specificed project with
2707 the contents specified.
2708
2709 :arg str project_name: The name of the project containing
2710 zuul.yaml (e.g., common-config)
2711
2712 :arg str source_name: The path to the file (underneath the
2713 test fixture directory) whose contents should be used to
2714 replace zuul.yaml.
2715 """
2716
2717 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002718 files = {}
2719 with open(source_path, 'r') as f:
2720 data = f.read()
2721 layout = yaml.safe_load(data)
2722 files['zuul.yaml'] = data
2723 for item in layout:
2724 if 'job' in item:
2725 jobname = item['job']['name']
2726 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002727 before = self.addCommitToRepo(
2728 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002729 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002730 return before
2731
James E. Blair7fc8daa2016-08-08 15:37:15 -07002732 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002733
James E. Blair7fc8daa2016-08-08 15:37:15 -07002734 """Inject a Fake (Gerrit) event.
2735
2736 This method accepts a JSON-encoded event and simulates Zuul
2737 having received it from Gerrit. It could (and should)
2738 eventually apply to any connection type, but is currently only
2739 used with Gerrit connections. The name of the connection is
2740 used to look up the corresponding server, and the event is
2741 simulated as having been received by all Zuul connections
2742 attached to that server. So if two Gerrit connections in Zuul
2743 are connected to the same Gerrit server, and you invoke this
2744 method specifying the name of one of them, the event will be
2745 received by both.
2746
2747 .. note::
2748
2749 "self.fake_gerrit.addEvent" calls should be migrated to
2750 this method.
2751
2752 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002753 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002754 :arg str event: The JSON-encoded event.
2755
2756 """
2757 specified_conn = self.connections.connections[connection]
2758 for conn in self.connections.connections.values():
2759 if (isinstance(conn, specified_conn.__class__) and
2760 specified_conn.server == conn.server):
2761 conn.addEvent(event)
2762
James E. Blaird8af5422017-05-24 13:59:40 -07002763 def getUpstreamRepos(self, projects):
2764 """Return upstream git repo objects for the listed projects
2765
2766 :arg list projects: A list of strings, each the canonical name
2767 of a project.
2768
2769 :returns: A dictionary of {name: repo} for every listed
2770 project.
2771 :rtype: dict
2772
2773 """
2774
2775 repos = {}
2776 for project in projects:
2777 # FIXME(jeblair): the upstream root does not yet have a
2778 # hostname component; that needs to be added, and this
2779 # line removed:
2780 tmp_project_name = '/'.join(project.split('/')[1:])
2781 path = os.path.join(self.upstream_root, tmp_project_name)
2782 repo = git.Repo(path)
2783 repos[project] = repo
2784 return repos
2785
James E. Blair3f876d52016-07-22 13:07:14 -07002786
2787class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002788 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002789 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002790
Jamie Lennox7655b552017-03-17 12:33:38 +11002791 @contextmanager
2792 def jobLog(self, build):
2793 """Print job logs on assertion errors
2794
2795 This method is a context manager which, if it encounters an
2796 ecxeption, adds the build log to the debug output.
2797
2798 :arg Build build: The build that's being asserted.
2799 """
2800 try:
2801 yield
2802 except Exception:
2803 path = os.path.join(self.test_root, build.uuid,
2804 'work', 'logs', 'job-output.txt')
2805 with open(path) as f:
2806 self.log.debug(f.read())
2807 raise
2808
Joshua Heskethd78b4482015-09-14 16:56:34 -06002809
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002810class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002811 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002812 use_ssl = True
2813
2814
Joshua Heskethd78b4482015-09-14 16:56:34 -06002815class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002816 def setup_config(self):
2817 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002818 for section_name in self.config.sections():
2819 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2820 section_name, re.I)
2821 if not con_match:
2822 continue
2823
2824 if self.config.get(section_name, 'driver') == 'sql':
2825 f = MySQLSchemaFixture()
2826 self.useFixture(f)
2827 if (self.config.get(section_name, 'dburi') ==
2828 '$MYSQL_FIXTURE_DBURI$'):
2829 self.config.set(section_name, 'dburi', f.dburi)