blob: 680c2a7bbf7765a1a00ef4873be04d0d5954a9e2 [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. Blair289f5932017-07-27 15:02:29 -0700142 status='NEW', upstream_root=None, files={},
143 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700145 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.reported = 0
147 self.queried = 0
148 self.patchsets = []
149 self.number = number
150 self.project = project
151 self.branch = branch
152 self.subject = subject
153 self.latest_patchset = 0
154 self.depends_on_change = None
155 self.needed_by_changes = []
156 self.fail_merge = False
157 self.messages = []
158 self.data = {
159 'branch': branch,
160 'comments': [],
161 'commitMessage': subject,
162 'createdOn': time.time(),
163 'id': 'I' + random_sha1(),
164 'lastUpdated': time.time(),
165 'number': str(number),
166 'open': status == 'NEW',
167 'owner': {'email': 'user@example.com',
168 'name': 'User Name',
169 'username': 'username'},
170 'patchSets': self.patchsets,
171 'project': project,
172 'status': status,
173 'subject': subject,
174 'submitRecords': [],
175 'url': 'https://hostname/%s' % number}
176
177 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700178 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 self.data['submitRecords'] = self.getSubmitRecords()
180 self.open = status == 'NEW'
181
James E. Blair289f5932017-07-27 15:02:29 -0700182 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 path = os.path.join(self.upstream_root, self.project)
184 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700185 if parent is None:
186 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700187 ref = GerritChangeReference.create(
188 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700189 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700191 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700192 repo.git.clean('-x', '-f', '-d')
193
194 path = os.path.join(self.upstream_root, self.project)
195 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700196 for fn, content in files.items():
197 fn = os.path.join(path, fn)
198 with open(fn, 'w') as f:
199 f.write(content)
200 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700201 else:
202 for fni in range(100):
203 fn = os.path.join(path, str(fni))
204 f = open(fn, 'w')
205 for ci in range(4096):
206 f.write(random.choice(string.printable))
207 f.close()
208 repo.index.add([fn])
209
210 r = repo.index.commit(msg)
211 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700212 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700213 repo.git.clean('-x', '-f', '-d')
214 repo.heads['master'].checkout()
215 return r
216
James E. Blair289f5932017-07-27 15:02:29 -0700217 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700218 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700219 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700220 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 data = ("test %s %s %s\n" %
222 (self.branch, self.number, self.latest_patchset))
223 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700224 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700225 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700226 ps_files = [{'file': '/COMMIT_MSG',
227 'type': 'ADDED'},
228 {'file': 'README',
229 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700230 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700231 ps_files.append({'file': f, 'type': 'ADDED'})
232 d = {'approvals': [],
233 'createdOn': time.time(),
234 'files': ps_files,
235 'number': str(self.latest_patchset),
236 'ref': 'refs/changes/1/%s/%s' % (self.number,
237 self.latest_patchset),
238 'revision': c.hexsha,
239 'uploader': {'email': 'user@example.com',
240 'name': 'User name',
241 'username': 'user'}}
242 self.data['currentPatchSet'] = d
243 self.patchsets.append(d)
244 self.data['submitRecords'] = self.getSubmitRecords()
245
246 def getPatchsetCreatedEvent(self, patchset):
247 event = {"type": "patchset-created",
248 "change": {"project": self.project,
249 "branch": self.branch,
250 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
251 "number": str(self.number),
252 "subject": self.subject,
253 "owner": {"name": "User Name"},
254 "url": "https://hostname/3"},
255 "patchSet": self.patchsets[patchset - 1],
256 "uploader": {"name": "User Name"}}
257 return event
258
259 def getChangeRestoredEvent(self):
260 event = {"type": "change-restored",
261 "change": {"project": self.project,
262 "branch": self.branch,
263 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
264 "number": str(self.number),
265 "subject": self.subject,
266 "owner": {"name": "User Name"},
267 "url": "https://hostname/3"},
268 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100269 "patchSet": self.patchsets[-1],
270 "reason": ""}
271 return event
272
273 def getChangeAbandonedEvent(self):
274 event = {"type": "change-abandoned",
275 "change": {"project": self.project,
276 "branch": self.branch,
277 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
278 "number": str(self.number),
279 "subject": self.subject,
280 "owner": {"name": "User Name"},
281 "url": "https://hostname/3"},
282 "abandoner": {"name": "User Name"},
283 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700284 "reason": ""}
285 return event
286
287 def getChangeCommentEvent(self, patchset):
288 event = {"type": "comment-added",
289 "change": {"project": self.project,
290 "branch": self.branch,
291 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
292 "number": str(self.number),
293 "subject": self.subject,
294 "owner": {"name": "User Name"},
295 "url": "https://hostname/3"},
296 "patchSet": self.patchsets[patchset - 1],
297 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200298 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700299 "description": "Code-Review",
300 "value": "0"}],
301 "comment": "This is a comment"}
302 return event
303
James E. Blairc2a5ed72017-02-20 14:12:01 -0500304 def getChangeMergedEvent(self):
305 event = {"submitter": {"name": "Jenkins",
306 "username": "jenkins"},
307 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
308 "patchSet": self.patchsets[-1],
309 "change": self.data,
310 "type": "change-merged",
311 "eventCreatedOn": 1487613810}
312 return event
313
James E. Blair8cce42e2016-10-18 08:18:36 -0700314 def getRefUpdatedEvent(self):
315 path = os.path.join(self.upstream_root, self.project)
316 repo = git.Repo(path)
317 oldrev = repo.heads[self.branch].commit.hexsha
318
319 event = {
320 "type": "ref-updated",
321 "submitter": {
322 "name": "User Name",
323 },
324 "refUpdate": {
325 "oldRev": oldrev,
326 "newRev": self.patchsets[-1]['revision'],
327 "refName": self.branch,
328 "project": self.project,
329 }
330 }
331 return event
332
Joshua Hesketh642824b2014-07-01 17:54:59 +1000333 def addApproval(self, category, value, username='reviewer_john',
334 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700335 if not granted_on:
336 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000337 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200338 'description': self.categories[category][0],
339 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000340 'value': str(value),
341 'by': {
342 'username': username,
343 'email': username + '@example.com',
344 },
345 'grantedOn': int(granted_on)
346 }
Clark Boylanb640e052014-04-03 16:41:46 -0700347 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200348 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700349 del self.patchsets[-1]['approvals'][i]
350 self.patchsets[-1]['approvals'].append(approval)
351 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000352 'author': {'email': 'author@example.com',
353 'name': 'Patchset Author',
354 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700355 'change': {'branch': self.branch,
356 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
357 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000358 'owner': {'email': 'owner@example.com',
359 'name': 'Change Owner',
360 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700361 'project': self.project,
362 'subject': self.subject,
363 'topic': 'master',
364 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000365 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700366 'patchSet': self.patchsets[-1],
367 'type': 'comment-added'}
368 self.data['submitRecords'] = self.getSubmitRecords()
369 return json.loads(json.dumps(event))
370
371 def getSubmitRecords(self):
372 status = {}
373 for cat in self.categories.keys():
374 status[cat] = 0
375
376 for a in self.patchsets[-1]['approvals']:
377 cur = status[a['type']]
378 cat_min, cat_max = self.categories[a['type']][1:]
379 new = int(a['value'])
380 if new == cat_min:
381 cur = new
382 elif abs(new) > abs(cur):
383 cur = new
384 status[a['type']] = cur
385
386 labels = []
387 ok = True
388 for typ, cat in self.categories.items():
389 cur = status[typ]
390 cat_min, cat_max = cat[1:]
391 if cur == cat_min:
392 value = 'REJECT'
393 ok = False
394 elif cur == cat_max:
395 value = 'OK'
396 else:
397 value = 'NEED'
398 ok = False
399 labels.append({'label': cat[0], 'status': value})
400 if ok:
401 return [{'status': 'OK'}]
402 return [{'status': 'NOT_READY',
403 'labels': labels}]
404
405 def setDependsOn(self, other, patchset):
406 self.depends_on_change = other
407 d = {'id': other.data['id'],
408 'number': other.data['number'],
409 'ref': other.patchsets[patchset - 1]['ref']
410 }
411 self.data['dependsOn'] = [d]
412
413 other.needed_by_changes.append(self)
414 needed = other.data.get('neededBy', [])
415 d = {'id': self.data['id'],
416 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700417 'ref': self.patchsets[-1]['ref'],
418 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700419 }
420 needed.append(d)
421 other.data['neededBy'] = needed
422
423 def query(self):
424 self.queried += 1
425 d = self.data.get('dependsOn')
426 if d:
427 d = d[0]
428 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
429 d['isCurrentPatchSet'] = True
430 else:
431 d['isCurrentPatchSet'] = False
432 return json.loads(json.dumps(self.data))
433
434 def setMerged(self):
435 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000436 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700437 return
438 if self.fail_merge:
439 return
440 self.data['status'] = 'MERGED'
441 self.open = False
442
443 path = os.path.join(self.upstream_root, self.project)
444 repo = git.Repo(path)
445 repo.heads[self.branch].commit = \
446 repo.commit(self.patchsets[-1]['revision'])
447
448 def setReported(self):
449 self.reported += 1
450
451
James E. Blaire511d2f2016-12-08 15:22:26 -0800452class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700453 """A Fake Gerrit connection for use in tests.
454
455 This subclasses
456 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
457 ability for tests to add changes to the fake Gerrit it represents.
458 """
459
Joshua Hesketh352264b2015-08-11 23:42:08 +1000460 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700461
James E. Blaire511d2f2016-12-08 15:22:26 -0800462 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700463 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800464 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 connection_config)
466
Monty Taylorb934c1a2017-06-16 19:31:47 -0500467 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700468 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
469 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700471 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200472 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700473
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700474 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700475 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700476 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700478 c = FakeGerritChange(self, self.change_number, project, branch,
479 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700480 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700481 self.changes[self.change_number] = c
482 return c
483
James E. Blair72facdc2017-08-17 10:29:12 -0700484 def getFakeBranchCreatedEvent(self, project, branch):
485 path = os.path.join(self.upstream_root, project)
486 repo = git.Repo(path)
487 oldrev = 40 * '0'
488
489 event = {
490 "type": "ref-updated",
491 "submitter": {
492 "name": "User Name",
493 },
494 "refUpdate": {
495 "oldRev": oldrev,
496 "newRev": repo.heads[branch].commit.hexsha,
497 "refName": branch,
498 "project": project,
499 }
500 }
501 return event
502
Clark Boylanb640e052014-04-03 16:41:46 -0700503 def review(self, project, changeid, message, action):
504 number, ps = changeid.split(',')
505 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000506
507 # Add the approval back onto the change (ie simulate what gerrit would
508 # do).
509 # Usually when zuul leaves a review it'll create a feedback loop where
510 # zuul's review enters another gerrit event (which is then picked up by
511 # zuul). However, we can't mimic this behaviour (by adding this
512 # approval event into the queue) as it stops jobs from checking what
513 # happens before this event is triggered. If a job needs to see what
514 # happens they can add their own verified event into the queue.
515 # Nevertheless, we can update change with the new review in gerrit.
516
James E. Blair8b5408c2016-08-08 15:37:46 -0700517 for cat in action.keys():
518 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000519 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000520
Clark Boylanb640e052014-04-03 16:41:46 -0700521 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000522
Clark Boylanb640e052014-04-03 16:41:46 -0700523 if 'submit' in action:
524 change.setMerged()
525 if message:
526 change.setReported()
527
528 def query(self, number):
529 change = self.changes.get(int(number))
530 if change:
531 return change.query()
532 return {}
533
James E. Blairc494d542014-08-06 09:23:52 -0700534 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700535 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700536 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800537 if query.startswith('change:'):
538 # Query a specific changeid
539 changeid = query[len('change:'):]
540 l = [change.query() for change in self.changes.values()
541 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700542 elif query.startswith('message:'):
543 # Query the content of a commit message
544 msg = query[len('message:'):].strip()
545 l = [change.query() for change in self.changes.values()
546 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800547 else:
548 # Query all open changes
549 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700550 return l
James E. Blairc494d542014-08-06 09:23:52 -0700551
Joshua Hesketh352264b2015-08-11 23:42:08 +1000552 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700553 pass
554
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200555 def _uploadPack(self, project):
556 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
557 'multi_ack thin-pack side-band side-band-64k ofs-delta '
558 'shallow no-progress include-tag multi_ack_detailed no-done\n')
559 path = os.path.join(self.upstream_root, project.name)
560 repo = git.Repo(path)
561 for ref in repo.refs:
562 r = ref.object.hexsha + ' ' + ref.path + '\n'
563 ret += '%04x%s' % (len(r) + 4, r)
564 ret += '0000'
565 return ret
566
Joshua Hesketh352264b2015-08-11 23:42:08 +1000567 def getGitUrl(self, project):
568 return os.path.join(self.upstream_root, project.name)
569
Clark Boylanb640e052014-04-03 16:41:46 -0700570
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571class GithubChangeReference(git.Reference):
572 _common_path_default = "refs/pull"
573 _points_to_commits_only = True
574
575
Tobias Henkel64e37a02017-08-02 10:13:30 +0200576class FakeGithub(object):
577
578 class FakeUser(object):
579 def __init__(self, login):
580 self.login = login
581 self.name = "Github User"
582 self.email = "github.user@example.com"
583
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200584 class FakeBranch(object):
585 def __init__(self, branch='master'):
586 self.name = branch
587
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200588 class FakeStatus(object):
589 def __init__(self, state, url, description, context, user):
590 self._state = state
591 self._url = url
592 self._description = description
593 self._context = context
594 self._user = user
595
596 def as_dict(self):
597 return {
598 'state': self._state,
599 'url': self._url,
600 'description': self._description,
601 'context': self._context,
602 'creator': {
603 'login': self._user
604 }
605 }
606
607 class FakeCommit(object):
608 def __init__(self):
609 self._statuses = []
610
611 def set_status(self, state, url, description, context, user):
612 status = FakeGithub.FakeStatus(
613 state, url, description, context, user)
614 # always insert a status to the front of the list, to represent
615 # the last status provided for a commit.
616 self._statuses.insert(0, status)
617
618 def statuses(self):
619 return self._statuses
620
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200621 class FakeRepository(object):
622 def __init__(self):
623 self._branches = [FakeGithub.FakeBranch()]
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200624 self._commits = {}
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200625
Tobias Henkeleca46202017-08-02 20:27:10 +0200626 def branches(self, protected=False):
627 if protected:
628 # simulate there is no protected branch
629 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200630 return self._branches
631
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200632 def create_status(self, sha, state, url, description, context,
633 user='zuul'):
634 # Since we're bypassing github API, which would require a user, we
635 # default the user as 'zuul' here.
636 commit = self._commits.get(sha, None)
637 if commit is None:
638 commit = FakeGithub.FakeCommit()
639 self._commits[sha] = commit
640 commit.set_status(state, url, description, context, user)
641
642 def commit(self, sha):
643 commit = self._commits.get(sha, None)
644 if commit is None:
645 commit = FakeGithub.FakeCommit()
646 self._commits[sha] = commit
647 return commit
648
649 def __init__(self):
650 self._repos = {}
651
Tobias Henkel64e37a02017-08-02 10:13:30 +0200652 def user(self, login):
653 return self.FakeUser(login)
654
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200655 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200656 return self._repos.get((owner, proj), None)
657
658 def repo_from_project(self, project):
659 # This is a convenience method for the tests.
660 owner, proj = project.split('/')
661 return self.repository(owner, proj)
662
663 def addProject(self, project):
664 owner, proj = project.name.split('/')
665 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200666
Tobias Henkel64e37a02017-08-02 10:13:30 +0200667
Gregory Haynes4fc12542015-04-22 20:38:06 -0700668class FakeGithubPullRequest(object):
669
670 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800671 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700672 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700673 """Creates a new PR with several commits.
674 Sends an event about opened PR."""
675 self.github = github
676 self.source = github
677 self.number = number
678 self.project = project
679 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100680 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700681 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100682 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700683 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100684 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700685 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100686 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100687 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800688 self.reviews = []
689 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700690 self.updated_at = None
691 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100692 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100693 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700694 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700695 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100696 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700697 self._updateTimeStamp()
698
Jan Hruban570d01c2016-03-10 21:51:32 +0100699 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700700 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100701 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700702 self._updateTimeStamp()
703
Jan Hruban570d01c2016-03-10 21:51:32 +0100704 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700705 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100706 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700707 self._updateTimeStamp()
708
709 def getPullRequestOpenedEvent(self):
710 return self._getPullRequestEvent('opened')
711
712 def getPullRequestSynchronizeEvent(self):
713 return self._getPullRequestEvent('synchronize')
714
715 def getPullRequestReopenedEvent(self):
716 return self._getPullRequestEvent('reopened')
717
718 def getPullRequestClosedEvent(self):
719 return self._getPullRequestEvent('closed')
720
Jesse Keatinga41566f2017-06-14 18:17:51 -0700721 def getPullRequestEditedEvent(self):
722 return self._getPullRequestEvent('edited')
723
Gregory Haynes4fc12542015-04-22 20:38:06 -0700724 def addComment(self, message):
725 self.comments.append(message)
726 self._updateTimeStamp()
727
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200728 def getCommentAddedEvent(self, text):
729 name = 'issue_comment'
730 data = {
731 'action': 'created',
732 'issue': {
733 'number': self.number
734 },
735 'comment': {
736 'body': text
737 },
738 'repository': {
739 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100740 },
741 'sender': {
742 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200743 }
744 }
745 return (name, data)
746
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800747 def getReviewAddedEvent(self, review):
748 name = 'pull_request_review'
749 data = {
750 'action': 'submitted',
751 'pull_request': {
752 'number': self.number,
753 'title': self.subject,
754 'updated_at': self.updated_at,
755 'base': {
756 'ref': self.branch,
757 'repo': {
758 'full_name': self.project
759 }
760 },
761 'head': {
762 'sha': self.head_sha
763 }
764 },
765 'review': {
766 'state': review
767 },
768 'repository': {
769 'full_name': self.project
770 },
771 'sender': {
772 'login': 'ghuser'
773 }
774 }
775 return (name, data)
776
Jan Hruban16ad31f2015-11-07 14:39:07 +0100777 def addLabel(self, name):
778 if name not in self.labels:
779 self.labels.append(name)
780 self._updateTimeStamp()
781 return self._getLabelEvent(name)
782
783 def removeLabel(self, name):
784 if name in self.labels:
785 self.labels.remove(name)
786 self._updateTimeStamp()
787 return self._getUnlabelEvent(name)
788
789 def _getLabelEvent(self, label):
790 name = 'pull_request'
791 data = {
792 'action': 'labeled',
793 'pull_request': {
794 'number': self.number,
795 'updated_at': self.updated_at,
796 'base': {
797 'ref': self.branch,
798 'repo': {
799 'full_name': self.project
800 }
801 },
802 'head': {
803 'sha': self.head_sha
804 }
805 },
806 'label': {
807 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100808 },
809 'sender': {
810 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100811 }
812 }
813 return (name, data)
814
815 def _getUnlabelEvent(self, label):
816 name = 'pull_request'
817 data = {
818 'action': 'unlabeled',
819 'pull_request': {
820 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100821 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100822 'updated_at': self.updated_at,
823 'base': {
824 'ref': self.branch,
825 'repo': {
826 'full_name': self.project
827 }
828 },
829 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800830 'sha': self.head_sha,
831 'repo': {
832 'full_name': self.project
833 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100834 }
835 },
836 'label': {
837 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100838 },
839 'sender': {
840 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100841 }
842 }
843 return (name, data)
844
Jesse Keatinga41566f2017-06-14 18:17:51 -0700845 def editBody(self, body):
846 self.body = body
847 self._updateTimeStamp()
848
Gregory Haynes4fc12542015-04-22 20:38:06 -0700849 def _getRepo(self):
850 repo_path = os.path.join(self.upstream_root, self.project)
851 return git.Repo(repo_path)
852
853 def _createPRRef(self):
854 repo = self._getRepo()
855 GithubChangeReference.create(
856 repo, self._getPRReference(), 'refs/tags/init')
857
Jan Hruban570d01c2016-03-10 21:51:32 +0100858 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700859 repo = self._getRepo()
860 ref = repo.references[self._getPRReference()]
861 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100862 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700863 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100864 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700865 repo.head.reference = ref
866 zuul.merger.merger.reset_repo_to_head(repo)
867 repo.git.clean('-x', '-f', '-d')
868
Jan Hruban570d01c2016-03-10 21:51:32 +0100869 if files:
870 fn = files[0]
871 self.files = files
872 else:
873 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
874 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100875 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700876 fn = os.path.join(repo.working_dir, fn)
877 f = open(fn, 'w')
878 with open(fn, 'w') as f:
879 f.write("test %s %s\n" %
880 (self.branch, self.number))
881 repo.index.add([fn])
882
883 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800884 # Create an empty set of statuses for the given sha,
885 # each sha on a PR may have a status set on it
886 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700887 repo.head.reference = 'master'
888 zuul.merger.merger.reset_repo_to_head(repo)
889 repo.git.clean('-x', '-f', '-d')
890 repo.heads['master'].checkout()
891
892 def _updateTimeStamp(self):
893 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
894
895 def getPRHeadSha(self):
896 repo = self._getRepo()
897 return repo.references[self._getPRReference()].commit.hexsha
898
Jesse Keatingae4cd272017-01-30 17:10:44 -0800899 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800900 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
901 # convert the timestamp to a str format that would be returned
902 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800903
Adam Gandelmand81dd762017-02-09 15:15:49 -0800904 if granted_on:
905 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
906 submitted_at = time.strftime(
907 gh_time_format, granted_on.timetuple())
908 else:
909 # github timestamps only down to the second, so we need to make
910 # sure reviews that tests add appear to be added over a period of
911 # time in the past and not all at once.
912 if not self.reviews:
913 # the first review happens 10 mins ago
914 offset = 600
915 else:
916 # subsequent reviews happen 1 minute closer to now
917 offset = 600 - (len(self.reviews) * 60)
918
919 granted_on = datetime.datetime.utcfromtimestamp(
920 time.time() - offset)
921 submitted_at = time.strftime(
922 gh_time_format, granted_on.timetuple())
923
Jesse Keatingae4cd272017-01-30 17:10:44 -0800924 self.reviews.append({
925 'state': state,
926 'user': {
927 'login': user,
928 'email': user + "@derp.com",
929 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800930 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800931 })
932
Gregory Haynes4fc12542015-04-22 20:38:06 -0700933 def _getPRReference(self):
934 return '%s/head' % self.number
935
936 def _getPullRequestEvent(self, action):
937 name = 'pull_request'
938 data = {
939 'action': action,
940 'number': self.number,
941 'pull_request': {
942 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100943 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700944 'updated_at': self.updated_at,
945 'base': {
946 'ref': self.branch,
947 'repo': {
948 'full_name': self.project
949 }
950 },
951 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800952 'sha': self.head_sha,
953 'repo': {
954 'full_name': self.project
955 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700956 },
957 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100958 },
959 'sender': {
960 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700961 }
962 }
963 return (name, data)
964
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800965 def getCommitStatusEvent(self, context, state='success', user='zuul'):
966 name = 'status'
967 data = {
968 'state': state,
969 'sha': self.head_sha,
970 'description': 'Test results for %s: %s' % (self.head_sha, state),
971 'target_url': 'http://zuul/%s' % self.head_sha,
972 'branches': [],
973 'context': context,
974 'sender': {
975 'login': user
976 }
977 }
978 return (name, data)
979
James E. Blair289f5932017-07-27 15:02:29 -0700980 def setMerged(self, commit_message):
981 self.is_merged = True
982 self.merge_message = commit_message
983
984 repo = self._getRepo()
985 repo.heads[self.branch].commit = repo.commit(self.head_sha)
986
Gregory Haynes4fc12542015-04-22 20:38:06 -0700987
988class FakeGithubConnection(githubconnection.GithubConnection):
989 log = logging.getLogger("zuul.test.FakeGithubConnection")
990
991 def __init__(self, driver, connection_name, connection_config,
992 upstream_root=None):
993 super(FakeGithubConnection, self).__init__(driver, connection_name,
994 connection_config)
995 self.connection_name = connection_name
996 self.pr_number = 0
997 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700998 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700999 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001000 self.merge_failure = False
1001 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001002 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +02001003 self.github_client = FakeGithub()
1004
1005 def getGithubClient(self,
1006 project=None,
1007 user_id=None,
1008 use_app=True):
1009 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001010
Jesse Keatinga41566f2017-06-14 18:17:51 -07001011 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001012 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001013 self.pr_number += 1
1014 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001015 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001016 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001017 self.pull_requests.append(pull_request)
1018 return pull_request
1019
Jesse Keating71a47ff2017-06-06 11:36:43 -07001020 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1021 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001022 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001023 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001024 if not new_rev:
1025 new_rev = random_sha1()
1026 name = 'push'
1027 data = {
1028 'ref': ref,
1029 'before': old_rev,
1030 'after': new_rev,
1031 'repository': {
1032 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001033 },
1034 'commits': [
1035 {
1036 'added': added_files,
1037 'removed': removed_files,
1038 'modified': modified_files
1039 }
1040 ]
Wayne1a78c612015-06-11 17:14:13 -07001041 }
1042 return (name, data)
1043
Gregory Haynes4fc12542015-04-22 20:38:06 -07001044 def emitEvent(self, event):
1045 """Emulates sending the GitHub webhook event to the connection."""
1046 port = self.webapp.server.socket.getsockname()[1]
1047 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001048 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001049 secret = self.connection_config['webhook_token']
1050 signature = githubconnection._sign_request(payload, secret)
1051 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001052 req = urllib.request.Request(
1053 'http://localhost:%s/connection/%s/payload'
1054 % (port, self.connection_name),
1055 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001056 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001057
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001058 def addProject(self, project):
1059 # use the original method here and additionally register it in the
1060 # fake github
1061 super(FakeGithubConnection, self).addProject(project)
1062 self.getGithubClient(project).addProject(project)
1063
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001064 def getPull(self, project, number):
1065 pr = self.pull_requests[number - 1]
1066 data = {
1067 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001068 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001069 'updated_at': pr.updated_at,
1070 'base': {
1071 'repo': {
1072 'full_name': pr.project
1073 },
1074 'ref': pr.branch,
1075 },
Jan Hruban37615e52015-11-19 14:30:49 +01001076 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001077 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001078 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001079 'sha': pr.head_sha,
1080 'repo': {
1081 'full_name': pr.project
1082 }
Jesse Keating61040e72017-06-08 15:08:27 -07001083 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001084 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001085 'labels': pr.labels,
1086 'merged': pr.is_merged,
1087 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001088 }
1089 return data
1090
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001091 def getPullBySha(self, sha):
1092 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
1093 if len(prs) > 1:
1094 raise Exception('Multiple pulls found with head sha: %s' % sha)
1095 pr = prs[0]
1096 return self.getPull(pr.project, pr.number)
1097
Jesse Keatingae4cd272017-01-30 17:10:44 -08001098 def _getPullReviews(self, owner, project, number):
1099 pr = self.pull_requests[number - 1]
1100 return pr.reviews
1101
Jesse Keatingae4cd272017-01-30 17:10:44 -08001102 def getRepoPermission(self, project, login):
1103 owner, proj = project.split('/')
1104 for pr in self.pull_requests:
1105 pr_owner, pr_project = pr.project.split('/')
1106 if (pr_owner == owner and proj == pr_project):
1107 if login in pr.writers:
1108 return 'write'
1109 else:
1110 return 'read'
1111
Gregory Haynes4fc12542015-04-22 20:38:06 -07001112 def getGitUrl(self, project):
1113 return os.path.join(self.upstream_root, str(project))
1114
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001115 def real_getGitUrl(self, project):
1116 return super(FakeGithubConnection, self).getGitUrl(project)
1117
Jan Hrubane252a732017-01-03 15:03:09 +01001118 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001119 # record that this got reported
1120 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001121 pull_request = self.pull_requests[pr_number - 1]
1122 pull_request.addComment(message)
1123
Jan Hruban3b415922016-02-03 13:10:22 +01001124 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001125 # record that this got reported
1126 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001127 pull_request = self.pull_requests[pr_number - 1]
1128 if self.merge_failure:
1129 raise Exception('Pull request was not merged')
1130 if self.merge_not_allowed_count > 0:
1131 self.merge_not_allowed_count -= 1
1132 raise MergeFailure('Merge was not successful due to mergeability'
1133 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001134 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001135
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001136 def setCommitStatus(self, project, sha, state, url='', description='',
1137 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001138 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001139 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001140 super(FakeGithubConnection, self).setCommitStatus(
1141 project, sha, state,
1142 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001143
Jan Hruban16ad31f2015-11-07 14:39:07 +01001144 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001145 # record that this got reported
1146 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001147 pull_request = self.pull_requests[pr_number - 1]
1148 pull_request.addLabel(label)
1149
1150 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001151 # record that this got reported
1152 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001153 pull_request = self.pull_requests[pr_number - 1]
1154 pull_request.removeLabel(label)
1155
Jesse Keatinga41566f2017-06-14 18:17:51 -07001156 def _getNeededByFromPR(self, change):
1157 prs = []
1158 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001159 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001160 change.number))
1161 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001162 if not pr.body:
1163 body = ''
1164 else:
1165 body = pr.body
1166 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001167 # Get our version of a pull so that it's a dict
1168 pull = self.getPull(pr.project, pr.number)
1169 prs.append(pull)
1170
1171 return prs
1172
Gregory Haynes4fc12542015-04-22 20:38:06 -07001173
Clark Boylanb640e052014-04-03 16:41:46 -07001174class BuildHistory(object):
1175 def __init__(self, **kw):
1176 self.__dict__.update(kw)
1177
1178 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001179 return ("<Completed build, result: %s name: %s uuid: %s "
1180 "changes: %s ref: %s>" %
1181 (self.result, self.name, self.uuid,
1182 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001183
1184
Clark Boylanb640e052014-04-03 16:41:46 -07001185class FakeStatsd(threading.Thread):
1186 def __init__(self):
1187 threading.Thread.__init__(self)
1188 self.daemon = True
1189 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1190 self.sock.bind(('', 0))
1191 self.port = self.sock.getsockname()[1]
1192 self.wake_read, self.wake_write = os.pipe()
1193 self.stats = []
1194
1195 def run(self):
1196 while True:
1197 poll = select.poll()
1198 poll.register(self.sock, select.POLLIN)
1199 poll.register(self.wake_read, select.POLLIN)
1200 ret = poll.poll()
1201 for (fd, event) in ret:
1202 if fd == self.sock.fileno():
1203 data = self.sock.recvfrom(1024)
1204 if not data:
1205 return
1206 self.stats.append(data[0])
1207 if fd == self.wake_read:
1208 return
1209
1210 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001211 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001212
1213
James E. Blaire1767bc2016-08-02 10:00:27 -07001214class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001215 log = logging.getLogger("zuul.test")
1216
Paul Belanger174a8272017-03-14 13:20:10 -04001217 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001218 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001219 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001220 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001221 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001222 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001223 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001224 # TODOv3(jeblair): self.node is really "the label of the node
1225 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001226 # keep using it like this, or we may end up exposing more of
1227 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001228 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001229 self.node = None
1230 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001231 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001232 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001233 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001234 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001235 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001236 self.wait_condition = threading.Condition()
1237 self.waiting = False
1238 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001239 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001240 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001241 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001242 items = self.parameters['zuul']['items']
1243 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1244 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001245
James E. Blair3158e282016-08-19 09:34:11 -07001246 def __repr__(self):
1247 waiting = ''
1248 if self.waiting:
1249 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001250 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1251 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001252
Clark Boylanb640e052014-04-03 16:41:46 -07001253 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001254 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001255 self.wait_condition.acquire()
1256 self.wait_condition.notify()
1257 self.waiting = False
1258 self.log.debug("Build %s released" % self.unique)
1259 self.wait_condition.release()
1260
1261 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001262 """Return whether this build is being held.
1263
1264 :returns: Whether the build is being held.
1265 :rtype: bool
1266 """
1267
Clark Boylanb640e052014-04-03 16:41:46 -07001268 self.wait_condition.acquire()
1269 if self.waiting:
1270 ret = True
1271 else:
1272 ret = False
1273 self.wait_condition.release()
1274 return ret
1275
1276 def _wait(self):
1277 self.wait_condition.acquire()
1278 self.waiting = True
1279 self.log.debug("Build %s waiting" % self.unique)
1280 self.wait_condition.wait()
1281 self.wait_condition.release()
1282
1283 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001284 self.log.debug('Running build %s' % self.unique)
1285
Paul Belanger174a8272017-03-14 13:20:10 -04001286 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001287 self.log.debug('Holding build %s' % self.unique)
1288 self._wait()
1289 self.log.debug("Build %s continuing" % self.unique)
1290
James E. Blair412fba82017-01-26 15:00:50 -08001291 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001292 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001293 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001294 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001295 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001296 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001297 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001298
James E. Blaire1767bc2016-08-02 10:00:27 -07001299 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001300
James E. Blaira5dba232016-08-08 15:53:24 -07001301 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001302 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001303 for change in changes:
1304 if self.hasChanges(change):
1305 return True
1306 return False
1307
James E. Blaire7b99a02016-08-05 14:27:34 -07001308 def hasChanges(self, *changes):
1309 """Return whether this build has certain changes in its git repos.
1310
1311 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001312 are expected to be present (in order) in the git repository of
1313 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001314
1315 :returns: Whether the build has the indicated changes.
1316 :rtype: bool
1317
1318 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001319 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001320 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001321 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001322 try:
1323 repo = git.Repo(path)
1324 except NoSuchPathError as e:
1325 self.log.debug('%s' % e)
1326 return False
James E. Blair247cab72017-07-20 16:52:36 -07001327 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001328 commit_message = '%s-1' % change.subject
1329 self.log.debug("Checking if build %s has changes; commit_message "
1330 "%s; repo_messages %s" % (self, commit_message,
1331 repo_messages))
1332 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001333 self.log.debug(" messages do not match")
1334 return False
1335 self.log.debug(" OK")
1336 return True
1337
James E. Blaird8af5422017-05-24 13:59:40 -07001338 def getWorkspaceRepos(self, projects):
1339 """Return workspace git repo objects for the listed projects
1340
1341 :arg list projects: A list of strings, each the canonical name
1342 of a project.
1343
1344 :returns: A dictionary of {name: repo} for every listed
1345 project.
1346 :rtype: dict
1347
1348 """
1349
1350 repos = {}
1351 for project in projects:
1352 path = os.path.join(self.jobdir.src_root, project)
1353 repo = git.Repo(path)
1354 repos[project] = repo
1355 return repos
1356
Clark Boylanb640e052014-04-03 16:41:46 -07001357
Paul Belanger174a8272017-03-14 13:20:10 -04001358class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1359 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001360
Paul Belanger174a8272017-03-14 13:20:10 -04001361 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001362 they will report that they have started but then pause until
1363 released before reporting completion. This attribute may be
1364 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001365 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001366 be explicitly released.
1367
1368 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001369 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001370 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001371 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001372 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001373 self.hold_jobs_in_build = False
1374 self.lock = threading.Lock()
1375 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001376 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001377 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001378 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001379
James E. Blaira5dba232016-08-08 15:53:24 -07001380 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001381 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001382
1383 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001384 :arg Change change: The :py:class:`~tests.base.FakeChange`
1385 instance which should cause the job to fail. This job
1386 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001387
1388 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001389 l = self.fail_tests.get(name, [])
1390 l.append(change)
1391 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001392
James E. Blair962220f2016-08-03 11:22:38 -07001393 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001394 """Release a held build.
1395
1396 :arg str regex: A regular expression which, if supplied, will
1397 cause only builds with matching names to be released. If
1398 not supplied, all builds will be released.
1399
1400 """
James E. Blair962220f2016-08-03 11:22:38 -07001401 builds = self.running_builds[:]
1402 self.log.debug("Releasing build %s (%s)" % (regex,
1403 len(self.running_builds)))
1404 for build in builds:
1405 if not regex or re.match(regex, build.name):
1406 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001407 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001408 build.release()
1409 else:
1410 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001411 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001412 self.log.debug("Done releasing builds %s (%s)" %
1413 (regex, len(self.running_builds)))
1414
Paul Belanger174a8272017-03-14 13:20:10 -04001415 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001416 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001417 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001418 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001419 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001420 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001421 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001422 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001423 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1424 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001425
1426 def stopJob(self, job):
1427 self.log.debug("handle stop")
1428 parameters = json.loads(job.arguments)
1429 uuid = parameters['uuid']
1430 for build in self.running_builds:
1431 if build.unique == uuid:
1432 build.aborted = True
1433 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001434 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001435
James E. Blaira002b032017-04-18 10:35:48 -07001436 def stop(self):
1437 for build in self.running_builds:
1438 build.release()
1439 super(RecordingExecutorServer, self).stop()
1440
Joshua Hesketh50c21782016-10-13 21:34:14 +11001441
Paul Belanger174a8272017-03-14 13:20:10 -04001442class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001443 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001444 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001445 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001446 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001447 if not commit: # merge conflict
1448 self.recordResult('MERGER_FAILURE')
1449 return commit
1450
1451 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001452 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001453 self.executor_server.lock.acquire()
1454 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001455 BuildHistory(name=build.name, result=result, changes=build.changes,
1456 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001457 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001458 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001459 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001460 )
Paul Belanger174a8272017-03-14 13:20:10 -04001461 self.executor_server.running_builds.remove(build)
1462 del self.executor_server.job_builds[self.job.unique]
1463 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001464
1465 def runPlaybooks(self, args):
1466 build = self.executor_server.job_builds[self.job.unique]
1467 build.jobdir = self.jobdir
1468
1469 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1470 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001471 return result
1472
James E. Blair892cca62017-08-09 11:36:58 -07001473 def runAnsible(self, cmd, timeout, playbook):
Paul Belanger174a8272017-03-14 13:20:10 -04001474 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001475
Paul Belanger174a8272017-03-14 13:20:10 -04001476 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001477 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair892cca62017-08-09 11:36:58 -07001478 cmd, timeout, playbook)
James E. Blair412fba82017-01-26 15:00:50 -08001479 else:
1480 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001481 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001482
James E. Blairad8dca02017-02-21 11:48:32 -05001483 def getHostList(self, args):
1484 self.log.debug("hostlist")
1485 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001486 for host in hosts:
1487 host['host_vars']['ansible_connection'] = 'local'
1488
1489 hosts.append(dict(
1490 name='localhost',
1491 host_vars=dict(ansible_connection='local'),
1492 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001493 return hosts
1494
James E. Blairf5dbd002015-12-23 15:26:17 -08001495
Clark Boylanb640e052014-04-03 16:41:46 -07001496class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001497 """A Gearman server for use in tests.
1498
1499 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1500 added to the queue but will not be distributed to workers
1501 until released. This attribute may be changed at any time and
1502 will take effect for subsequently enqueued jobs, but
1503 previously held jobs will still need to be explicitly
1504 released.
1505
1506 """
1507
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001508 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001509 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001510 if use_ssl:
1511 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1512 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1513 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1514 else:
1515 ssl_ca = None
1516 ssl_cert = None
1517 ssl_key = None
1518
1519 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1520 ssl_cert=ssl_cert,
1521 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001522
1523 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001524 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1525 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001526 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001527 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001528 job.waiting = self.hold_jobs_in_queue
1529 else:
1530 job.waiting = False
1531 if job.waiting:
1532 continue
1533 if job.name in connection.functions:
1534 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001535 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001536 connection.related_jobs[job.handle] = job
1537 job.worker_connection = connection
1538 job.running = True
1539 return job
1540 return None
1541
1542 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001543 """Release a held job.
1544
1545 :arg str regex: A regular expression which, if supplied, will
1546 cause only jobs with matching names to be released. If
1547 not supplied, all jobs will be released.
1548 """
Clark Boylanb640e052014-04-03 16:41:46 -07001549 released = False
1550 qlen = (len(self.high_queue) + len(self.normal_queue) +
1551 len(self.low_queue))
1552 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1553 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001554 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001555 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001556 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001557 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001558 self.log.debug("releasing queued job %s" %
1559 job.unique)
1560 job.waiting = False
1561 released = True
1562 else:
1563 self.log.debug("not releasing queued job %s" %
1564 job.unique)
1565 if released:
1566 self.wakeConnections()
1567 qlen = (len(self.high_queue) + len(self.normal_queue) +
1568 len(self.low_queue))
1569 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1570
1571
1572class FakeSMTP(object):
1573 log = logging.getLogger('zuul.FakeSMTP')
1574
1575 def __init__(self, messages, server, port):
1576 self.server = server
1577 self.port = port
1578 self.messages = messages
1579
1580 def sendmail(self, from_email, to_email, msg):
1581 self.log.info("Sending email from %s, to %s, with msg %s" % (
1582 from_email, to_email, msg))
1583
1584 headers = msg.split('\n\n', 1)[0]
1585 body = msg.split('\n\n', 1)[1]
1586
1587 self.messages.append(dict(
1588 from_email=from_email,
1589 to_email=to_email,
1590 msg=msg,
1591 headers=headers,
1592 body=body,
1593 ))
1594
1595 return True
1596
1597 def quit(self):
1598 return True
1599
1600
James E. Blairdce6cea2016-12-20 16:45:32 -08001601class FakeNodepool(object):
1602 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001603 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001604
1605 log = logging.getLogger("zuul.test.FakeNodepool")
1606
1607 def __init__(self, host, port, chroot):
1608 self.client = kazoo.client.KazooClient(
1609 hosts='%s:%s%s' % (host, port, chroot))
1610 self.client.start()
1611 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001612 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001613 self.thread = threading.Thread(target=self.run)
1614 self.thread.daemon = True
1615 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001616 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001617
1618 def stop(self):
1619 self._running = False
1620 self.thread.join()
1621 self.client.stop()
1622 self.client.close()
1623
1624 def run(self):
1625 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001626 try:
1627 self._run()
1628 except Exception:
1629 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001630 time.sleep(0.1)
1631
1632 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001633 if self.paused:
1634 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001635 for req in self.getNodeRequests():
1636 self.fulfillRequest(req)
1637
1638 def getNodeRequests(self):
1639 try:
1640 reqids = self.client.get_children(self.REQUEST_ROOT)
1641 except kazoo.exceptions.NoNodeError:
1642 return []
1643 reqs = []
1644 for oid in sorted(reqids):
1645 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001646 try:
1647 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001648 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001649 data['_oid'] = oid
1650 reqs.append(data)
1651 except kazoo.exceptions.NoNodeError:
1652 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001653 return reqs
1654
James E. Blaire18d4602017-01-05 11:17:28 -08001655 def getNodes(self):
1656 try:
1657 nodeids = self.client.get_children(self.NODE_ROOT)
1658 except kazoo.exceptions.NoNodeError:
1659 return []
1660 nodes = []
1661 for oid in sorted(nodeids):
1662 path = self.NODE_ROOT + '/' + oid
1663 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001664 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001665 data['_oid'] = oid
1666 try:
1667 lockfiles = self.client.get_children(path + '/lock')
1668 except kazoo.exceptions.NoNodeError:
1669 lockfiles = []
1670 if lockfiles:
1671 data['_lock'] = True
1672 else:
1673 data['_lock'] = False
1674 nodes.append(data)
1675 return nodes
1676
James E. Blaira38c28e2017-01-04 10:33:20 -08001677 def makeNode(self, request_id, node_type):
1678 now = time.time()
1679 path = '/nodepool/nodes/'
1680 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001681 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001682 provider='test-provider',
1683 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001684 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001685 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001686 public_ipv4='127.0.0.1',
1687 private_ipv4=None,
1688 public_ipv6=None,
1689 allocated_to=request_id,
1690 state='ready',
1691 state_time=now,
1692 created_time=now,
1693 updated_time=now,
1694 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001695 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001696 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001697 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001698 path = self.client.create(path, data,
1699 makepath=True,
1700 sequence=True)
1701 nodeid = path.split("/")[-1]
1702 return nodeid
1703
James E. Blair6ab79e02017-01-06 10:10:17 -08001704 def addFailRequest(self, request):
1705 self.fail_requests.add(request['_oid'])
1706
James E. Blairdce6cea2016-12-20 16:45:32 -08001707 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001708 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001709 return
1710 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001711 oid = request['_oid']
1712 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001713
James E. Blair6ab79e02017-01-06 10:10:17 -08001714 if oid in self.fail_requests:
1715 request['state'] = 'failed'
1716 else:
1717 request['state'] = 'fulfilled'
1718 nodes = []
1719 for node in request['node_types']:
1720 nodeid = self.makeNode(oid, node)
1721 nodes.append(nodeid)
1722 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001723
James E. Blaira38c28e2017-01-04 10:33:20 -08001724 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001725 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001726 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001727 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001728 try:
1729 self.client.set(path, data)
1730 except kazoo.exceptions.NoNodeError:
1731 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001732
1733
James E. Blair498059b2016-12-20 13:50:13 -08001734class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001735 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001736 super(ChrootedKazooFixture, self).__init__()
1737
1738 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1739 if ':' in zk_host:
1740 host, port = zk_host.split(':')
1741 else:
1742 host = zk_host
1743 port = None
1744
1745 self.zookeeper_host = host
1746
1747 if not port:
1748 self.zookeeper_port = 2181
1749 else:
1750 self.zookeeper_port = int(port)
1751
Clark Boylan621ec9a2017-04-07 17:41:33 -07001752 self.test_id = test_id
1753
James E. Blair498059b2016-12-20 13:50:13 -08001754 def _setUp(self):
1755 # Make sure the test chroot paths do not conflict
1756 random_bits = ''.join(random.choice(string.ascii_lowercase +
1757 string.ascii_uppercase)
1758 for x in range(8))
1759
Clark Boylan621ec9a2017-04-07 17:41:33 -07001760 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001761 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1762
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001763 self.addCleanup(self._cleanup)
1764
James E. Blair498059b2016-12-20 13:50:13 -08001765 # Ensure the chroot path exists and clean up any pre-existing znodes.
1766 _tmp_client = kazoo.client.KazooClient(
1767 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1768 _tmp_client.start()
1769
1770 if _tmp_client.exists(self.zookeeper_chroot):
1771 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1772
1773 _tmp_client.ensure_path(self.zookeeper_chroot)
1774 _tmp_client.stop()
1775 _tmp_client.close()
1776
James E. Blair498059b2016-12-20 13:50:13 -08001777 def _cleanup(self):
1778 '''Remove the chroot path.'''
1779 # Need a non-chroot'ed client to remove the chroot path
1780 _tmp_client = kazoo.client.KazooClient(
1781 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1782 _tmp_client.start()
1783 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1784 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001785 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001786
1787
Joshua Heskethd78b4482015-09-14 16:56:34 -06001788class MySQLSchemaFixture(fixtures.Fixture):
1789 def setUp(self):
1790 super(MySQLSchemaFixture, self).setUp()
1791
1792 random_bits = ''.join(random.choice(string.ascii_lowercase +
1793 string.ascii_uppercase)
1794 for x in range(8))
1795 self.name = '%s_%s' % (random_bits, os.getpid())
1796 self.passwd = uuid.uuid4().hex
1797 db = pymysql.connect(host="localhost",
1798 user="openstack_citest",
1799 passwd="openstack_citest",
1800 db="openstack_citest")
1801 cur = db.cursor()
1802 cur.execute("create database %s" % self.name)
1803 cur.execute(
1804 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1805 (self.name, self.name, self.passwd))
1806 cur.execute("flush privileges")
1807
1808 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1809 self.passwd,
1810 self.name)
1811 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1812 self.addCleanup(self.cleanup)
1813
1814 def cleanup(self):
1815 db = pymysql.connect(host="localhost",
1816 user="openstack_citest",
1817 passwd="openstack_citest",
1818 db="openstack_citest")
1819 cur = db.cursor()
1820 cur.execute("drop database %s" % self.name)
1821 cur.execute("drop user '%s'@'localhost'" % self.name)
1822 cur.execute("flush privileges")
1823
1824
Maru Newby3fe5f852015-01-13 04:22:14 +00001825class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001826 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001827 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001828
James E. Blair1c236df2017-02-01 14:07:24 -08001829 def attachLogs(self, *args):
1830 def reader():
1831 self._log_stream.seek(0)
1832 while True:
1833 x = self._log_stream.read(4096)
1834 if not x:
1835 break
1836 yield x.encode('utf8')
1837 content = testtools.content.content_from_reader(
1838 reader,
1839 testtools.content_type.UTF8_TEXT,
1840 False)
1841 self.addDetail('logging', content)
1842
Clark Boylanb640e052014-04-03 16:41:46 -07001843 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001844 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001845 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1846 try:
1847 test_timeout = int(test_timeout)
1848 except ValueError:
1849 # If timeout value is invalid do not set a timeout.
1850 test_timeout = 0
1851 if test_timeout > 0:
1852 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1853
1854 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1855 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1856 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1857 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1858 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1859 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1860 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1861 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1862 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1863 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001864 self._log_stream = StringIO()
1865 self.addOnException(self.attachLogs)
1866 else:
1867 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001868
James E. Blair73b41772017-05-22 13:22:55 -07001869 # NOTE(jeblair): this is temporary extra debugging to try to
1870 # track down a possible leak.
1871 orig_git_repo_init = git.Repo.__init__
1872
1873 def git_repo_init(myself, *args, **kw):
1874 orig_git_repo_init(myself, *args, **kw)
1875 self.log.debug("Created git repo 0x%x %s" %
1876 (id(myself), repr(myself)))
1877
1878 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1879 git_repo_init))
1880
James E. Blair1c236df2017-02-01 14:07:24 -08001881 handler = logging.StreamHandler(self._log_stream)
1882 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1883 '%(levelname)-8s %(message)s')
1884 handler.setFormatter(formatter)
1885
1886 logger = logging.getLogger()
1887 logger.setLevel(logging.DEBUG)
1888 logger.addHandler(handler)
1889
Clark Boylan3410d532017-04-25 12:35:29 -07001890 # Make sure we don't carry old handlers around in process state
1891 # which slows down test runs
1892 self.addCleanup(logger.removeHandler, handler)
1893 self.addCleanup(handler.close)
1894 self.addCleanup(handler.flush)
1895
James E. Blair1c236df2017-02-01 14:07:24 -08001896 # NOTE(notmorgan): Extract logging overrides for specific
1897 # libraries from the OS_LOG_DEFAULTS env and create loggers
1898 # for each. This is used to limit the output during test runs
1899 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001900 log_defaults_from_env = os.environ.get(
1901 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001902 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001903
James E. Blairdce6cea2016-12-20 16:45:32 -08001904 if log_defaults_from_env:
1905 for default in log_defaults_from_env.split(','):
1906 try:
1907 name, level_str = default.split('=', 1)
1908 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001909 logger = logging.getLogger(name)
1910 logger.setLevel(level)
1911 logger.addHandler(handler)
1912 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001913 except ValueError:
1914 # NOTE(notmorgan): Invalid format of the log default,
1915 # skip and don't try and apply a logger for the
1916 # specified module
1917 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001918
Maru Newby3fe5f852015-01-13 04:22:14 +00001919
1920class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001921 """A test case with a functioning Zuul.
1922
1923 The following class variables are used during test setup and can
1924 be overidden by subclasses but are effectively read-only once a
1925 test method starts running:
1926
1927 :cvar str config_file: This points to the main zuul config file
1928 within the fixtures directory. Subclasses may override this
1929 to obtain a different behavior.
1930
1931 :cvar str tenant_config_file: This is the tenant config file
1932 (which specifies from what git repos the configuration should
1933 be loaded). It defaults to the value specified in
1934 `config_file` but can be overidden by subclasses to obtain a
1935 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001936 configuration. See also the :py:func:`simple_layout`
1937 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001938
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001939 :cvar bool create_project_keys: Indicates whether Zuul should
1940 auto-generate keys for each project, or whether the test
1941 infrastructure should insert dummy keys to save time during
1942 startup. Defaults to False.
1943
James E. Blaire7b99a02016-08-05 14:27:34 -07001944 The following are instance variables that are useful within test
1945 methods:
1946
1947 :ivar FakeGerritConnection fake_<connection>:
1948 A :py:class:`~tests.base.FakeGerritConnection` will be
1949 instantiated for each connection present in the config file
1950 and stored here. For instance, `fake_gerrit` will hold the
1951 FakeGerritConnection object for a connection named `gerrit`.
1952
1953 :ivar FakeGearmanServer gearman_server: An instance of
1954 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1955 server that all of the Zuul components in this test use to
1956 communicate with each other.
1957
Paul Belanger174a8272017-03-14 13:20:10 -04001958 :ivar RecordingExecutorServer executor_server: An instance of
1959 :py:class:`~tests.base.RecordingExecutorServer` which is the
1960 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001961
1962 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1963 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001964 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001965 list upon completion.
1966
1967 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1968 objects representing completed builds. They are appended to
1969 the list in the order they complete.
1970
1971 """
1972
James E. Blair83005782015-12-11 14:46:03 -08001973 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001974 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001975 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001976 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001977
1978 def _startMerger(self):
1979 self.merge_server = zuul.merger.server.MergeServer(self.config,
1980 self.connections)
1981 self.merge_server.start()
1982
Maru Newby3fe5f852015-01-13 04:22:14 +00001983 def setUp(self):
1984 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001985
1986 self.setupZK()
1987
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001988 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001989 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001990 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1991 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001992 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001993 tmp_root = tempfile.mkdtemp(
1994 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001995 self.test_root = os.path.join(tmp_root, "zuul-test")
1996 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001997 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001998 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001999 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002000 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2001 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002002
2003 if os.path.exists(self.test_root):
2004 shutil.rmtree(self.test_root)
2005 os.makedirs(self.test_root)
2006 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002007 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002008 os.makedirs(self.merger_state_root)
2009 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002010
2011 # Make per test copy of Configuration.
2012 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002013 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2014 if not os.path.exists(self.private_key_file):
2015 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2016 shutil.copy(src_private_key_file, self.private_key_file)
2017 shutil.copy('{}.pub'.format(src_private_key_file),
2018 '{}.pub'.format(self.private_key_file))
2019 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002020 self.config.set('scheduler', 'tenant_config',
2021 os.path.join(
2022 FIXTURE_DIR,
2023 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002024 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002025 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002026 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002027 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002028 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002029
Clark Boylanb640e052014-04-03 16:41:46 -07002030 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10002031 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
2032 # see: https://github.com/jsocol/pystatsd/issues/61
2033 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07002034 os.environ['STATSD_PORT'] = str(self.statsd.port)
2035 self.statsd.start()
2036 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05002037 importlib.reload(statsd)
2038 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07002039
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002040 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002041
2042 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002043 self.log.info("Gearman server on port %s" %
2044 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002045 if self.use_ssl:
2046 self.log.info('SSL enabled for gearman')
2047 self.config.set(
2048 'gearman', 'ssl_ca',
2049 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2050 self.config.set(
2051 'gearman', 'ssl_cert',
2052 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2053 self.config.set(
2054 'gearman', 'ssl_key',
2055 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002056
James E. Blaire511d2f2016-12-08 15:22:26 -08002057 gerritsource.GerritSource.replication_timeout = 1.5
2058 gerritsource.GerritSource.replication_retry_interval = 0.5
2059 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002060
Joshua Hesketh352264b2015-08-11 23:42:08 +10002061 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07002062
Jan Hruban7083edd2015-08-21 14:00:54 +02002063 self.webapp = zuul.webapp.WebApp(
2064 self.sched, port=0, listen_address='127.0.0.1')
2065
Jan Hruban6b71aff2015-10-22 16:58:08 +02002066 self.event_queues = [
2067 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002068 self.sched.trigger_event_queue,
2069 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002070 ]
2071
James E. Blairfef78942016-03-11 16:28:56 -08002072 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002073 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002074
Paul Belanger174a8272017-03-14 13:20:10 -04002075 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002076 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002077 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002078 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002079 _test_root=self.test_root,
2080 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002081 self.executor_server.start()
2082 self.history = self.executor_server.build_history
2083 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002084
Paul Belanger174a8272017-03-14 13:20:10 -04002085 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002086 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002087 self.merge_client = zuul.merger.client.MergeClient(
2088 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002089 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002090 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002091 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002092
James E. Blair0d5a36e2017-02-21 10:53:44 -05002093 self.fake_nodepool = FakeNodepool(
2094 self.zk_chroot_fixture.zookeeper_host,
2095 self.zk_chroot_fixture.zookeeper_port,
2096 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002097
Paul Belanger174a8272017-03-14 13:20:10 -04002098 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002099 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002100 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002101 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002102
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002103 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002104
2105 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002106 self.webapp.start()
2107 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002108 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002109 # Cleanups are run in reverse order
2110 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002111 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002112 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002113
James E. Blairb9c0d772017-03-03 14:34:49 -08002114 self.sched.reconfigure(self.config)
2115 self.sched.resume()
2116
Tobias Henkel7df274b2017-05-26 17:41:11 +02002117 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002118 # Set up gerrit related fakes
2119 # Set a changes database so multiple FakeGerrit's can report back to
2120 # a virtual canonical database given by the configured hostname
2121 self.gerrit_changes_dbs = {}
2122
2123 def getGerritConnection(driver, name, config):
2124 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2125 con = FakeGerritConnection(driver, name, config,
2126 changes_db=db,
2127 upstream_root=self.upstream_root)
2128 self.event_queues.append(con.event_queue)
2129 setattr(self, 'fake_' + name, con)
2130 return con
2131
2132 self.useFixture(fixtures.MonkeyPatch(
2133 'zuul.driver.gerrit.GerritDriver.getConnection',
2134 getGerritConnection))
2135
Gregory Haynes4fc12542015-04-22 20:38:06 -07002136 def getGithubConnection(driver, name, config):
2137 con = FakeGithubConnection(driver, name, config,
2138 upstream_root=self.upstream_root)
2139 setattr(self, 'fake_' + name, con)
2140 return con
2141
2142 self.useFixture(fixtures.MonkeyPatch(
2143 'zuul.driver.github.GithubDriver.getConnection',
2144 getGithubConnection))
2145
James E. Blaire511d2f2016-12-08 15:22:26 -08002146 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002147 # TODO(jhesketh): This should come from lib.connections for better
2148 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002149 # Register connections from the config
2150 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002151
Joshua Hesketh352264b2015-08-11 23:42:08 +10002152 def FakeSMTPFactory(*args, **kw):
2153 args = [self.smtp_messages] + list(args)
2154 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002155
Joshua Hesketh352264b2015-08-11 23:42:08 +10002156 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002157
James E. Blaire511d2f2016-12-08 15:22:26 -08002158 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002159 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002160 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002161
James E. Blair83005782015-12-11 14:46:03 -08002162 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002163 # This creates the per-test configuration object. It can be
2164 # overriden by subclasses, but should not need to be since it
2165 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002166 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002167 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002168
James E. Blair39840362017-06-23 20:34:02 +01002169 sections = ['zuul', 'scheduler', 'executor', 'merger']
2170 for section in sections:
2171 if not self.config.has_section(section):
2172 self.config.add_section(section)
2173
James E. Blair06cc3922017-04-19 10:08:10 -07002174 if not self.setupSimpleLayout():
2175 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002176 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002177 self.tenant_config_file)
2178 git_path = os.path.join(
2179 os.path.dirname(
2180 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2181 'git')
2182 if os.path.exists(git_path):
2183 for reponame in os.listdir(git_path):
2184 project = reponame.replace('_', '/')
2185 self.copyDirToRepo(project,
2186 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002187 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002188 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002189 self.setupAllProjectKeys()
2190
James E. Blair06cc3922017-04-19 10:08:10 -07002191 def setupSimpleLayout(self):
2192 # If the test method has been decorated with a simple_layout,
2193 # use that instead of the class tenant_config_file. Set up a
2194 # single config-project with the specified layout, and
2195 # initialize repos for all of the 'project' entries which
2196 # appear in the layout.
2197 test_name = self.id().split('.')[-1]
2198 test = getattr(self, test_name)
2199 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002200 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002201 else:
2202 return False
2203
James E. Blairb70e55a2017-04-19 12:57:02 -07002204 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002205 path = os.path.join(FIXTURE_DIR, path)
2206 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002207 data = f.read()
2208 layout = yaml.safe_load(data)
2209 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002210 untrusted_projects = []
2211 for item in layout:
2212 if 'project' in item:
2213 name = item['project']['name']
2214 untrusted_projects.append(name)
2215 self.init_repo(name)
2216 self.addCommitToRepo(name, 'initial commit',
2217 files={'README': ''},
2218 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002219 if 'job' in item:
2220 jobname = item['job']['name']
2221 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002222
2223 root = os.path.join(self.test_root, "config")
2224 if not os.path.exists(root):
2225 os.makedirs(root)
2226 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2227 config = [{'tenant':
2228 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002229 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002230 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002231 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002232 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002233 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002234 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002235 os.path.join(FIXTURE_DIR, f.name))
2236
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002237 self.init_repo('org/common-config')
2238 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002239 files, branch='master', tag='init')
2240
2241 return True
2242
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002243 def setupAllProjectKeys(self):
2244 if self.create_project_keys:
2245 return
2246
James E. Blair39840362017-06-23 20:34:02 +01002247 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002248 with open(os.path.join(FIXTURE_DIR, path)) as f:
2249 tenant_config = yaml.safe_load(f.read())
2250 for tenant in tenant_config:
2251 sources = tenant['tenant']['source']
2252 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002253 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002254 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002255 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002256 self.setupProjectKeys(source, project)
2257
2258 def setupProjectKeys(self, source, project):
2259 # Make sure we set up an RSA key for the project so that we
2260 # don't spend time generating one:
2261
James E. Blair6459db12017-06-29 14:57:20 -07002262 if isinstance(project, dict):
2263 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002264 key_root = os.path.join(self.state_root, 'keys')
2265 if not os.path.isdir(key_root):
2266 os.mkdir(key_root, 0o700)
2267 private_key_file = os.path.join(key_root, source, project + '.pem')
2268 private_key_dir = os.path.dirname(private_key_file)
2269 self.log.debug("Installing test keys for project %s at %s" % (
2270 project, private_key_file))
2271 if not os.path.isdir(private_key_dir):
2272 os.makedirs(private_key_dir)
2273 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2274 with open(private_key_file, 'w') as o:
2275 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002276
James E. Blair498059b2016-12-20 13:50:13 -08002277 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002278 self.zk_chroot_fixture = self.useFixture(
2279 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002280 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002281 self.zk_chroot_fixture.zookeeper_host,
2282 self.zk_chroot_fixture.zookeeper_port,
2283 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002284
James E. Blair96c6bf82016-01-15 16:20:40 -08002285 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002286 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002287
2288 files = {}
2289 for (dirpath, dirnames, filenames) in os.walk(source_path):
2290 for filename in filenames:
2291 test_tree_filepath = os.path.join(dirpath, filename)
2292 common_path = os.path.commonprefix([test_tree_filepath,
2293 source_path])
2294 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2295 with open(test_tree_filepath, 'r') as f:
2296 content = f.read()
2297 files[relative_filepath] = content
2298 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002299 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002300
James E. Blaire18d4602017-01-05 11:17:28 -08002301 def assertNodepoolState(self):
2302 # Make sure that there are no pending requests
2303
2304 requests = self.fake_nodepool.getNodeRequests()
2305 self.assertEqual(len(requests), 0)
2306
2307 nodes = self.fake_nodepool.getNodes()
2308 for node in nodes:
2309 self.assertFalse(node['_lock'], "Node %s is locked" %
2310 (node['_oid'],))
2311
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002312 def assertNoGeneratedKeys(self):
2313 # Make sure that Zuul did not generate any project keys
2314 # (unless it was supposed to).
2315
2316 if self.create_project_keys:
2317 return
2318
2319 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2320 test_key = i.read()
2321
2322 key_root = os.path.join(self.state_root, 'keys')
2323 for root, dirname, files in os.walk(key_root):
2324 for fn in files:
2325 with open(os.path.join(root, fn)) as f:
2326 self.assertEqual(test_key, f.read())
2327
Clark Boylanb640e052014-04-03 16:41:46 -07002328 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002329 self.log.debug("Assert final state")
2330 # Make sure no jobs are running
2331 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002332 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002333 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002334 gc.collect()
2335 for obj in gc.get_objects():
2336 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002337 self.log.debug("Leaked git repo object: 0x%x %s" %
2338 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002339 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002340 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002341 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002342 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002343 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002344 for tenant in self.sched.abide.tenants.values():
2345 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002346 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002347 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002348
2349 def shutdown(self):
2350 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002351 self.executor_server.hold_jobs_in_build = False
2352 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002353 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002354 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002355 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002356 self.sched.stop()
2357 self.sched.join()
2358 self.statsd.stop()
2359 self.statsd.join()
2360 self.webapp.stop()
2361 self.webapp.join()
2362 self.rpc.stop()
2363 self.rpc.join()
2364 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002365 self.fake_nodepool.stop()
2366 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002367 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002368 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002369 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002370 # Further the pydevd threads also need to be whitelisted so debugging
2371 # e.g. in PyCharm is possible without breaking shutdown.
2372 whitelist = ['executor-watchdog',
2373 'pydevd.CommandThread',
2374 'pydevd.Reader',
2375 'pydevd.Writer',
2376 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002377 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002378 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002379 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002380 log_str = ""
2381 for thread_id, stack_frame in sys._current_frames().items():
2382 log_str += "Thread: %s\n" % thread_id
2383 log_str += "".join(traceback.format_stack(stack_frame))
2384 self.log.debug(log_str)
2385 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002386
James E. Blaira002b032017-04-18 10:35:48 -07002387 def assertCleanShutdown(self):
2388 pass
2389
James E. Blairc4ba97a2017-04-19 16:26:24 -07002390 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002391 parts = project.split('/')
2392 path = os.path.join(self.upstream_root, *parts[:-1])
2393 if not os.path.exists(path):
2394 os.makedirs(path)
2395 path = os.path.join(self.upstream_root, project)
2396 repo = git.Repo.init(path)
2397
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002398 with repo.config_writer() as config_writer:
2399 config_writer.set_value('user', 'email', 'user@example.com')
2400 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002401
Clark Boylanb640e052014-04-03 16:41:46 -07002402 repo.index.commit('initial commit')
2403 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002404 if tag:
2405 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002406
James E. Blair97d902e2014-08-21 13:25:56 -07002407 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002408 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002409 repo.git.clean('-x', '-f', '-d')
2410
James E. Blair97d902e2014-08-21 13:25:56 -07002411 def create_branch(self, project, branch):
2412 path = os.path.join(self.upstream_root, project)
2413 repo = git.Repo.init(path)
2414 fn = os.path.join(path, 'README')
2415
2416 branch_head = repo.create_head(branch)
2417 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002418 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002419 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002420 f.close()
2421 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002422 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002423
James E. Blair97d902e2014-08-21 13:25:56 -07002424 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002425 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002426 repo.git.clean('-x', '-f', '-d')
2427
Sachi King9f16d522016-03-16 12:20:45 +11002428 def create_commit(self, project):
2429 path = os.path.join(self.upstream_root, project)
2430 repo = git.Repo(path)
2431 repo.head.reference = repo.heads['master']
2432 file_name = os.path.join(path, 'README')
2433 with open(file_name, 'a') as f:
2434 f.write('creating fake commit\n')
2435 repo.index.add([file_name])
2436 commit = repo.index.commit('Creating a fake commit')
2437 return commit.hexsha
2438
James E. Blairf4a5f022017-04-18 14:01:10 -07002439 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002440 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002441 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002442 while len(self.builds):
2443 self.release(self.builds[0])
2444 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002445 i += 1
2446 if count is not None and i >= count:
2447 break
James E. Blairb8c16472015-05-05 14:55:26 -07002448
James E. Blairdf25ddc2017-07-08 07:57:09 -07002449 def getSortedBuilds(self):
2450 "Return the list of currently running builds sorted by name"
2451
2452 return sorted(self.builds, key=lambda x: x.name)
2453
Clark Boylanb640e052014-04-03 16:41:46 -07002454 def release(self, job):
2455 if isinstance(job, FakeBuild):
2456 job.release()
2457 else:
2458 job.waiting = False
2459 self.log.debug("Queued job %s released" % job.unique)
2460 self.gearman_server.wakeConnections()
2461
2462 def getParameter(self, job, name):
2463 if isinstance(job, FakeBuild):
2464 return job.parameters[name]
2465 else:
2466 parameters = json.loads(job.arguments)
2467 return parameters[name]
2468
Clark Boylanb640e052014-04-03 16:41:46 -07002469 def haveAllBuildsReported(self):
2470 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002471 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002472 return False
2473 # Find out if every build that the worker has completed has been
2474 # reported back to Zuul. If it hasn't then that means a Gearman
2475 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002476 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002477 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002478 if not zbuild:
2479 # It has already been reported
2480 continue
2481 # It hasn't been reported yet.
2482 return False
2483 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002484 worker = self.executor_server.executor_worker
2485 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002486 if connection.state == 'GRAB_WAIT':
2487 return False
2488 return True
2489
2490 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002491 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002492 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002493 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002494 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002495 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002496 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002497 for j in conn.related_jobs.values():
2498 if j.unique == build.uuid:
2499 client_job = j
2500 break
2501 if not client_job:
2502 self.log.debug("%s is not known to the gearman client" %
2503 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002504 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002505 if not client_job.handle:
2506 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002507 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002508 server_job = self.gearman_server.jobs.get(client_job.handle)
2509 if not server_job:
2510 self.log.debug("%s is not known to the gearman server" %
2511 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002512 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002513 if not hasattr(server_job, 'waiting'):
2514 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002515 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002516 if server_job.waiting:
2517 continue
James E. Blair17302972016-08-10 16:11:42 -07002518 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002519 self.log.debug("%s has not reported start" % build)
2520 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002521 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002522 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002523 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002524 if worker_build:
2525 if worker_build.isWaiting():
2526 continue
2527 else:
2528 self.log.debug("%s is running" % worker_build)
2529 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002530 else:
James E. Blair962220f2016-08-03 11:22:38 -07002531 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002532 return False
James E. Blaira002b032017-04-18 10:35:48 -07002533 for (build_uuid, job_worker) in \
2534 self.executor_server.job_workers.items():
2535 if build_uuid not in seen_builds:
2536 self.log.debug("%s is not finalized" % build_uuid)
2537 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002538 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002539
James E. Blairdce6cea2016-12-20 16:45:32 -08002540 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002541 if self.fake_nodepool.paused:
2542 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002543 if self.sched.nodepool.requests:
2544 return False
2545 return True
2546
Jan Hruban6b71aff2015-10-22 16:58:08 +02002547 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002548 for event_queue in self.event_queues:
2549 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002550
2551 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002552 for event_queue in self.event_queues:
2553 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002554
Clark Boylanb640e052014-04-03 16:41:46 -07002555 def waitUntilSettled(self):
2556 self.log.debug("Waiting until settled...")
2557 start = time.time()
2558 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002559 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002560 self.log.error("Timeout waiting for Zuul to settle")
2561 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002562 for event_queue in self.event_queues:
2563 self.log.error(" %s: %s" %
2564 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002565 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002566 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002567 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002568 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002569 self.log.error("All requests completed: %s" %
2570 (self.areAllNodeRequestsComplete(),))
2571 self.log.error("Merge client jobs: %s" %
2572 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002573 raise Exception("Timeout waiting for Zuul to settle")
2574 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002575
Paul Belanger174a8272017-03-14 13:20:10 -04002576 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002577 # have all build states propogated to zuul?
2578 if self.haveAllBuildsReported():
2579 # Join ensures that the queue is empty _and_ events have been
2580 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002581 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002582 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002583 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002584 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002585 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002586 self.areAllNodeRequestsComplete() and
2587 all(self.eventQueuesEmpty())):
2588 # The queue empty check is placed at the end to
2589 # ensure that if a component adds an event between
2590 # when locked the run handler and checked that the
2591 # components were stable, we don't erroneously
2592 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002593 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002594 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002595 self.log.debug("...settled.")
2596 return
2597 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002598 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002599 self.sched.wake_event.wait(0.1)
2600
2601 def countJobResults(self, jobs, result):
2602 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002603 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002604
Monty Taylor0d926122017-05-24 08:07:56 -05002605 def getBuildByName(self, name):
2606 for build in self.builds:
2607 if build.name == name:
2608 return build
2609 raise Exception("Unable to find build %s" % name)
2610
James E. Blair96c6bf82016-01-15 16:20:40 -08002611 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002612 for job in self.history:
2613 if (job.name == name and
2614 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002615 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002616 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002617 raise Exception("Unable to find job %s in history" % name)
2618
2619 def assertEmptyQueues(self):
2620 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002621 for tenant in self.sched.abide.tenants.values():
2622 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002623 for pipeline_queue in pipeline.queues:
2624 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002625 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002626 pipeline.name, pipeline_queue.name,
2627 pipeline_queue.queue))
2628 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002629 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002630
2631 def assertReportedStat(self, key, value=None, kind=None):
2632 start = time.time()
2633 while time.time() < (start + 5):
2634 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002635 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002636 if key == k:
2637 if value is None and kind is None:
2638 return
2639 elif value:
2640 if value == v:
2641 return
2642 elif kind:
2643 if v.endswith('|' + kind):
2644 return
2645 time.sleep(0.1)
2646
Clark Boylanb640e052014-04-03 16:41:46 -07002647 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002648
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002649 def assertBuilds(self, builds):
2650 """Assert that the running builds are as described.
2651
2652 The list of running builds is examined and must match exactly
2653 the list of builds described by the input.
2654
2655 :arg list builds: A list of dictionaries. Each item in the
2656 list must match the corresponding build in the build
2657 history, and each element of the dictionary must match the
2658 corresponding attribute of the build.
2659
2660 """
James E. Blair3158e282016-08-19 09:34:11 -07002661 try:
2662 self.assertEqual(len(self.builds), len(builds))
2663 for i, d in enumerate(builds):
2664 for k, v in d.items():
2665 self.assertEqual(
2666 getattr(self.builds[i], k), v,
2667 "Element %i in builds does not match" % (i,))
2668 except Exception:
2669 for build in self.builds:
2670 self.log.error("Running build: %s" % build)
2671 else:
2672 self.log.error("No running builds")
2673 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002674
James E. Blairb536ecc2016-08-31 10:11:42 -07002675 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002676 """Assert that the completed builds are as described.
2677
2678 The list of completed builds is examined and must match
2679 exactly the list of builds described by the input.
2680
2681 :arg list history: A list of dictionaries. Each item in the
2682 list must match the corresponding build in the build
2683 history, and each element of the dictionary must match the
2684 corresponding attribute of the build.
2685
James E. Blairb536ecc2016-08-31 10:11:42 -07002686 :arg bool ordered: If true, the history must match the order
2687 supplied, if false, the builds are permitted to have
2688 arrived in any order.
2689
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002690 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002691 def matches(history_item, item):
2692 for k, v in item.items():
2693 if getattr(history_item, k) != v:
2694 return False
2695 return True
James E. Blair3158e282016-08-19 09:34:11 -07002696 try:
2697 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002698 if ordered:
2699 for i, d in enumerate(history):
2700 if not matches(self.history[i], d):
2701 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002702 "Element %i in history does not match %s" %
2703 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002704 else:
2705 unseen = self.history[:]
2706 for i, d in enumerate(history):
2707 found = False
2708 for unseen_item in unseen:
2709 if matches(unseen_item, d):
2710 found = True
2711 unseen.remove(unseen_item)
2712 break
2713 if not found:
2714 raise Exception("No match found for element %i "
2715 "in history" % (i,))
2716 if unseen:
2717 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002718 except Exception:
2719 for build in self.history:
2720 self.log.error("Completed build: %s" % build)
2721 else:
2722 self.log.error("No completed builds")
2723 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002724
James E. Blair6ac368c2016-12-22 18:07:20 -08002725 def printHistory(self):
2726 """Log the build history.
2727
2728 This can be useful during tests to summarize what jobs have
2729 completed.
2730
2731 """
2732 self.log.debug("Build history:")
2733 for build in self.history:
2734 self.log.debug(build)
2735
James E. Blair59fdbac2015-12-07 17:08:06 -08002736 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002737 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2738
James E. Blair9ea70072017-04-19 16:05:30 -07002739 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002740 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002741 if not os.path.exists(root):
2742 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002743 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2744 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002745- tenant:
2746 name: openstack
2747 source:
2748 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002749 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002750 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002751 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002752 - org/project
2753 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002754 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002755 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002756 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002757 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002758 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002759
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002760 def addCommitToRepo(self, project, message, files,
2761 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002762 path = os.path.join(self.upstream_root, project)
2763 repo = git.Repo(path)
2764 repo.head.reference = branch
2765 zuul.merger.merger.reset_repo_to_head(repo)
2766 for fn, content in files.items():
2767 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002768 try:
2769 os.makedirs(os.path.dirname(fn))
2770 except OSError:
2771 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002772 with open(fn, 'w') as f:
2773 f.write(content)
2774 repo.index.add([fn])
2775 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002776 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002777 repo.heads[branch].commit = commit
2778 repo.head.reference = branch
2779 repo.git.clean('-x', '-f', '-d')
2780 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002781 if tag:
2782 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002783 return before
2784
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002785 def commitConfigUpdate(self, project_name, source_name):
2786 """Commit an update to zuul.yaml
2787
2788 This overwrites the zuul.yaml in the specificed project with
2789 the contents specified.
2790
2791 :arg str project_name: The name of the project containing
2792 zuul.yaml (e.g., common-config)
2793
2794 :arg str source_name: The path to the file (underneath the
2795 test fixture directory) whose contents should be used to
2796 replace zuul.yaml.
2797 """
2798
2799 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002800 files = {}
2801 with open(source_path, 'r') as f:
2802 data = f.read()
2803 layout = yaml.safe_load(data)
2804 files['zuul.yaml'] = data
2805 for item in layout:
2806 if 'job' in item:
2807 jobname = item['job']['name']
2808 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002809 before = self.addCommitToRepo(
2810 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002811 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002812 return before
2813
James E. Blair7fc8daa2016-08-08 15:37:15 -07002814 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002815
James E. Blair7fc8daa2016-08-08 15:37:15 -07002816 """Inject a Fake (Gerrit) event.
2817
2818 This method accepts a JSON-encoded event and simulates Zuul
2819 having received it from Gerrit. It could (and should)
2820 eventually apply to any connection type, but is currently only
2821 used with Gerrit connections. The name of the connection is
2822 used to look up the corresponding server, and the event is
2823 simulated as having been received by all Zuul connections
2824 attached to that server. So if two Gerrit connections in Zuul
2825 are connected to the same Gerrit server, and you invoke this
2826 method specifying the name of one of them, the event will be
2827 received by both.
2828
2829 .. note::
2830
2831 "self.fake_gerrit.addEvent" calls should be migrated to
2832 this method.
2833
2834 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002835 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002836 :arg str event: The JSON-encoded event.
2837
2838 """
2839 specified_conn = self.connections.connections[connection]
2840 for conn in self.connections.connections.values():
2841 if (isinstance(conn, specified_conn.__class__) and
2842 specified_conn.server == conn.server):
2843 conn.addEvent(event)
2844
James E. Blaird8af5422017-05-24 13:59:40 -07002845 def getUpstreamRepos(self, projects):
2846 """Return upstream git repo objects for the listed projects
2847
2848 :arg list projects: A list of strings, each the canonical name
2849 of a project.
2850
2851 :returns: A dictionary of {name: repo} for every listed
2852 project.
2853 :rtype: dict
2854
2855 """
2856
2857 repos = {}
2858 for project in projects:
2859 # FIXME(jeblair): the upstream root does not yet have a
2860 # hostname component; that needs to be added, and this
2861 # line removed:
2862 tmp_project_name = '/'.join(project.split('/')[1:])
2863 path = os.path.join(self.upstream_root, tmp_project_name)
2864 repo = git.Repo(path)
2865 repos[project] = repo
2866 return repos
2867
James E. Blair3f876d52016-07-22 13:07:14 -07002868
2869class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002870 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002871 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002872
Jamie Lennox7655b552017-03-17 12:33:38 +11002873 @contextmanager
2874 def jobLog(self, build):
2875 """Print job logs on assertion errors
2876
2877 This method is a context manager which, if it encounters an
2878 ecxeption, adds the build log to the debug output.
2879
2880 :arg Build build: The build that's being asserted.
2881 """
2882 try:
2883 yield
2884 except Exception:
2885 path = os.path.join(self.test_root, build.uuid,
2886 'work', 'logs', 'job-output.txt')
2887 with open(path) as f:
2888 self.log.debug(f.read())
2889 raise
2890
Joshua Heskethd78b4482015-09-14 16:56:34 -06002891
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002892class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002893 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002894 use_ssl = True
2895
2896
Joshua Heskethd78b4482015-09-14 16:56:34 -06002897class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002898 def setup_config(self):
2899 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002900 for section_name in self.config.sections():
2901 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2902 section_name, re.I)
2903 if not con_match:
2904 continue
2905
2906 if self.config.get(section_name, 'driver') == 'sql':
2907 f = MySQLSchemaFixture()
2908 self.useFixture(f)
2909 if (self.config.get(section_name, 'dburi') ==
2910 '$MYSQL_FIXTURE_DBURI$'):
2911 self.config.set(section_name, 'dburi', f.dburi)