blob: c49e1ce32a6738cbbf7edff2e6de9021652e87d5 [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,
Jesse Keating9021a012017-08-29 14:45:27 -0700970 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800971 'description': 'Test results for %s: %s' % (self.head_sha, state),
972 'target_url': 'http://zuul/%s' % self.head_sha,
973 'branches': [],
974 'context': context,
975 'sender': {
976 'login': user
977 }
978 }
979 return (name, data)
980
James E. Blair289f5932017-07-27 15:02:29 -0700981 def setMerged(self, commit_message):
982 self.is_merged = True
983 self.merge_message = commit_message
984
985 repo = self._getRepo()
986 repo.heads[self.branch].commit = repo.commit(self.head_sha)
987
Gregory Haynes4fc12542015-04-22 20:38:06 -0700988
989class FakeGithubConnection(githubconnection.GithubConnection):
990 log = logging.getLogger("zuul.test.FakeGithubConnection")
991
992 def __init__(self, driver, connection_name, connection_config,
993 upstream_root=None):
994 super(FakeGithubConnection, self).__init__(driver, connection_name,
995 connection_config)
996 self.connection_name = connection_name
997 self.pr_number = 0
998 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700999 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001000 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001001 self.merge_failure = False
1002 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001003 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +02001004 self.github_client = FakeGithub()
1005
1006 def getGithubClient(self,
1007 project=None,
1008 user_id=None,
1009 use_app=True):
1010 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001011
Jesse Keatinga41566f2017-06-14 18:17:51 -07001012 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001013 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001014 self.pr_number += 1
1015 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001016 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001017 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001018 self.pull_requests.append(pull_request)
1019 return pull_request
1020
Jesse Keating71a47ff2017-06-06 11:36:43 -07001021 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1022 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001023 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001024 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001025 if not new_rev:
1026 new_rev = random_sha1()
1027 name = 'push'
1028 data = {
1029 'ref': ref,
1030 'before': old_rev,
1031 'after': new_rev,
1032 'repository': {
1033 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001034 },
1035 'commits': [
1036 {
1037 'added': added_files,
1038 'removed': removed_files,
1039 'modified': modified_files
1040 }
1041 ]
Wayne1a78c612015-06-11 17:14:13 -07001042 }
1043 return (name, data)
1044
Gregory Haynes4fc12542015-04-22 20:38:06 -07001045 def emitEvent(self, event):
1046 """Emulates sending the GitHub webhook event to the connection."""
1047 port = self.webapp.server.socket.getsockname()[1]
1048 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001049 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001050 secret = self.connection_config['webhook_token']
1051 signature = githubconnection._sign_request(payload, secret)
1052 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001053 req = urllib.request.Request(
1054 'http://localhost:%s/connection/%s/payload'
1055 % (port, self.connection_name),
1056 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001057 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001058
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001059 def addProject(self, project):
1060 # use the original method here and additionally register it in the
1061 # fake github
1062 super(FakeGithubConnection, self).addProject(project)
1063 self.getGithubClient(project).addProject(project)
1064
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001065 def getPull(self, project, number):
1066 pr = self.pull_requests[number - 1]
1067 data = {
1068 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001069 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001070 'updated_at': pr.updated_at,
1071 'base': {
1072 'repo': {
1073 'full_name': pr.project
1074 },
1075 'ref': pr.branch,
1076 },
Jan Hruban37615e52015-11-19 14:30:49 +01001077 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001078 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001079 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001080 'sha': pr.head_sha,
1081 'repo': {
1082 'full_name': pr.project
1083 }
Jesse Keating61040e72017-06-08 15:08:27 -07001084 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001085 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001086 'labels': pr.labels,
1087 'merged': pr.is_merged,
1088 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001089 }
1090 return data
1091
Jesse Keating9021a012017-08-29 14:45:27 -07001092 def getPullBySha(self, sha, project):
1093 prs = list(set([p for p in self.pull_requests if
1094 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001095 if len(prs) > 1:
1096 raise Exception('Multiple pulls found with head sha: %s' % sha)
1097 pr = prs[0]
1098 return self.getPull(pr.project, pr.number)
1099
Jesse Keatingae4cd272017-01-30 17:10:44 -08001100 def _getPullReviews(self, owner, project, number):
1101 pr = self.pull_requests[number - 1]
1102 return pr.reviews
1103
Jesse Keatingae4cd272017-01-30 17:10:44 -08001104 def getRepoPermission(self, project, login):
1105 owner, proj = project.split('/')
1106 for pr in self.pull_requests:
1107 pr_owner, pr_project = pr.project.split('/')
1108 if (pr_owner == owner and proj == pr_project):
1109 if login in pr.writers:
1110 return 'write'
1111 else:
1112 return 'read'
1113
Gregory Haynes4fc12542015-04-22 20:38:06 -07001114 def getGitUrl(self, project):
1115 return os.path.join(self.upstream_root, str(project))
1116
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001117 def real_getGitUrl(self, project):
1118 return super(FakeGithubConnection, self).getGitUrl(project)
1119
Jan Hrubane252a732017-01-03 15:03:09 +01001120 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001121 # record that this got reported
1122 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001123 pull_request = self.pull_requests[pr_number - 1]
1124 pull_request.addComment(message)
1125
Jan Hruban3b415922016-02-03 13:10:22 +01001126 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001127 # record that this got reported
1128 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001129 pull_request = self.pull_requests[pr_number - 1]
1130 if self.merge_failure:
1131 raise Exception('Pull request was not merged')
1132 if self.merge_not_allowed_count > 0:
1133 self.merge_not_allowed_count -= 1
1134 raise MergeFailure('Merge was not successful due to mergeability'
1135 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001136 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001137
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001138 def setCommitStatus(self, project, sha, state, url='', description='',
1139 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001140 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001141 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001142 super(FakeGithubConnection, self).setCommitStatus(
1143 project, sha, state,
1144 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001145
Jan Hruban16ad31f2015-11-07 14:39:07 +01001146 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001147 # record that this got reported
1148 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001149 pull_request = self.pull_requests[pr_number - 1]
1150 pull_request.addLabel(label)
1151
1152 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001153 # record that this got reported
1154 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001155 pull_request = self.pull_requests[pr_number - 1]
1156 pull_request.removeLabel(label)
1157
Jesse Keatinga41566f2017-06-14 18:17:51 -07001158 def _getNeededByFromPR(self, change):
1159 prs = []
1160 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001161 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001162 change.number))
1163 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001164 if not pr.body:
1165 body = ''
1166 else:
1167 body = pr.body
1168 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001169 # Get our version of a pull so that it's a dict
1170 pull = self.getPull(pr.project, pr.number)
1171 prs.append(pull)
1172
1173 return prs
1174
Gregory Haynes4fc12542015-04-22 20:38:06 -07001175
Clark Boylanb640e052014-04-03 16:41:46 -07001176class BuildHistory(object):
1177 def __init__(self, **kw):
1178 self.__dict__.update(kw)
1179
1180 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001181 return ("<Completed build, result: %s name: %s uuid: %s "
1182 "changes: %s ref: %s>" %
1183 (self.result, self.name, self.uuid,
1184 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001185
1186
Clark Boylanb640e052014-04-03 16:41:46 -07001187class FakeStatsd(threading.Thread):
1188 def __init__(self):
1189 threading.Thread.__init__(self)
1190 self.daemon = True
1191 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1192 self.sock.bind(('', 0))
1193 self.port = self.sock.getsockname()[1]
1194 self.wake_read, self.wake_write = os.pipe()
1195 self.stats = []
1196
1197 def run(self):
1198 while True:
1199 poll = select.poll()
1200 poll.register(self.sock, select.POLLIN)
1201 poll.register(self.wake_read, select.POLLIN)
1202 ret = poll.poll()
1203 for (fd, event) in ret:
1204 if fd == self.sock.fileno():
1205 data = self.sock.recvfrom(1024)
1206 if not data:
1207 return
1208 self.stats.append(data[0])
1209 if fd == self.wake_read:
1210 return
1211
1212 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001213 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001214
1215
James E. Blaire1767bc2016-08-02 10:00:27 -07001216class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001217 log = logging.getLogger("zuul.test")
1218
Paul Belanger174a8272017-03-14 13:20:10 -04001219 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001220 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001221 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001222 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001223 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001224 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001225 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001226 # TODOv3(jeblair): self.node is really "the label of the node
1227 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001228 # keep using it like this, or we may end up exposing more of
1229 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001230 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001231 self.node = None
1232 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001233 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001234 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001235 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001236 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001237 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001238 self.wait_condition = threading.Condition()
1239 self.waiting = False
1240 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001241 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001242 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001243 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001244 items = self.parameters['zuul']['items']
1245 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1246 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001247
James E. Blair3158e282016-08-19 09:34:11 -07001248 def __repr__(self):
1249 waiting = ''
1250 if self.waiting:
1251 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001252 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1253 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001254
Clark Boylanb640e052014-04-03 16:41:46 -07001255 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001256 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001257 self.wait_condition.acquire()
1258 self.wait_condition.notify()
1259 self.waiting = False
1260 self.log.debug("Build %s released" % self.unique)
1261 self.wait_condition.release()
1262
1263 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001264 """Return whether this build is being held.
1265
1266 :returns: Whether the build is being held.
1267 :rtype: bool
1268 """
1269
Clark Boylanb640e052014-04-03 16:41:46 -07001270 self.wait_condition.acquire()
1271 if self.waiting:
1272 ret = True
1273 else:
1274 ret = False
1275 self.wait_condition.release()
1276 return ret
1277
1278 def _wait(self):
1279 self.wait_condition.acquire()
1280 self.waiting = True
1281 self.log.debug("Build %s waiting" % self.unique)
1282 self.wait_condition.wait()
1283 self.wait_condition.release()
1284
1285 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001286 self.log.debug('Running build %s' % self.unique)
1287
Paul Belanger174a8272017-03-14 13:20:10 -04001288 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001289 self.log.debug('Holding build %s' % self.unique)
1290 self._wait()
1291 self.log.debug("Build %s continuing" % self.unique)
1292
James E. Blair412fba82017-01-26 15:00:50 -08001293 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001294 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001295 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001296 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001297 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001298 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001299 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001300
James E. Blaire1767bc2016-08-02 10:00:27 -07001301 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001302
James E. Blaira5dba232016-08-08 15:53:24 -07001303 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001304 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001305 for change in changes:
1306 if self.hasChanges(change):
1307 return True
1308 return False
1309
James E. Blaire7b99a02016-08-05 14:27:34 -07001310 def hasChanges(self, *changes):
1311 """Return whether this build has certain changes in its git repos.
1312
1313 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001314 are expected to be present (in order) in the git repository of
1315 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001316
1317 :returns: Whether the build has the indicated changes.
1318 :rtype: bool
1319
1320 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001321 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001322 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001323 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001324 try:
1325 repo = git.Repo(path)
1326 except NoSuchPathError as e:
1327 self.log.debug('%s' % e)
1328 return False
James E. Blair247cab72017-07-20 16:52:36 -07001329 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001330 commit_message = '%s-1' % change.subject
1331 self.log.debug("Checking if build %s has changes; commit_message "
1332 "%s; repo_messages %s" % (self, commit_message,
1333 repo_messages))
1334 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001335 self.log.debug(" messages do not match")
1336 return False
1337 self.log.debug(" OK")
1338 return True
1339
James E. Blaird8af5422017-05-24 13:59:40 -07001340 def getWorkspaceRepos(self, projects):
1341 """Return workspace git repo objects for the listed projects
1342
1343 :arg list projects: A list of strings, each the canonical name
1344 of a project.
1345
1346 :returns: A dictionary of {name: repo} for every listed
1347 project.
1348 :rtype: dict
1349
1350 """
1351
1352 repos = {}
1353 for project in projects:
1354 path = os.path.join(self.jobdir.src_root, project)
1355 repo = git.Repo(path)
1356 repos[project] = repo
1357 return repos
1358
Clark Boylanb640e052014-04-03 16:41:46 -07001359
Paul Belanger174a8272017-03-14 13:20:10 -04001360class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1361 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001362
Paul Belanger174a8272017-03-14 13:20:10 -04001363 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001364 they will report that they have started but then pause until
1365 released before reporting completion. This attribute may be
1366 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001367 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001368 be explicitly released.
1369
1370 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001371 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001372 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001373 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001374 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001375 self.hold_jobs_in_build = False
1376 self.lock = threading.Lock()
1377 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001378 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001379 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001380 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001381
James E. Blaira5dba232016-08-08 15:53:24 -07001382 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001383 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001384
1385 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001386 :arg Change change: The :py:class:`~tests.base.FakeChange`
1387 instance which should cause the job to fail. This job
1388 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001389
1390 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001391 l = self.fail_tests.get(name, [])
1392 l.append(change)
1393 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001394
James E. Blair962220f2016-08-03 11:22:38 -07001395 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001396 """Release a held build.
1397
1398 :arg str regex: A regular expression which, if supplied, will
1399 cause only builds with matching names to be released. If
1400 not supplied, all builds will be released.
1401
1402 """
James E. Blair962220f2016-08-03 11:22:38 -07001403 builds = self.running_builds[:]
1404 self.log.debug("Releasing build %s (%s)" % (regex,
1405 len(self.running_builds)))
1406 for build in builds:
1407 if not regex or re.match(regex, build.name):
1408 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001409 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001410 build.release()
1411 else:
1412 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001413 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001414 self.log.debug("Done releasing builds %s (%s)" %
1415 (regex, len(self.running_builds)))
1416
Paul Belanger174a8272017-03-14 13:20:10 -04001417 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001418 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001419 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001420 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001421 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001422 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001423 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001424 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001425 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1426 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001427
1428 def stopJob(self, job):
1429 self.log.debug("handle stop")
1430 parameters = json.loads(job.arguments)
1431 uuid = parameters['uuid']
1432 for build in self.running_builds:
1433 if build.unique == uuid:
1434 build.aborted = True
1435 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001436 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001437
James E. Blaira002b032017-04-18 10:35:48 -07001438 def stop(self):
1439 for build in self.running_builds:
1440 build.release()
1441 super(RecordingExecutorServer, self).stop()
1442
Joshua Hesketh50c21782016-10-13 21:34:14 +11001443
Paul Belanger174a8272017-03-14 13:20:10 -04001444class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001445 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001446 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001447 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001448 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001449 if not commit: # merge conflict
1450 self.recordResult('MERGER_FAILURE')
1451 return commit
1452
1453 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001454 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001455 self.executor_server.lock.acquire()
1456 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001457 BuildHistory(name=build.name, result=result, changes=build.changes,
1458 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001459 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001460 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001461 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001462 )
Paul Belanger174a8272017-03-14 13:20:10 -04001463 self.executor_server.running_builds.remove(build)
1464 del self.executor_server.job_builds[self.job.unique]
1465 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001466
1467 def runPlaybooks(self, args):
1468 build = self.executor_server.job_builds[self.job.unique]
1469 build.jobdir = self.jobdir
1470
1471 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1472 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001473 return result
1474
James E. Blair892cca62017-08-09 11:36:58 -07001475 def runAnsible(self, cmd, timeout, playbook):
Paul Belanger174a8272017-03-14 13:20:10 -04001476 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001477
Paul Belanger174a8272017-03-14 13:20:10 -04001478 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001479 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair892cca62017-08-09 11:36:58 -07001480 cmd, timeout, playbook)
James E. Blair412fba82017-01-26 15:00:50 -08001481 else:
1482 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001483 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001484
James E. Blairad8dca02017-02-21 11:48:32 -05001485 def getHostList(self, args):
1486 self.log.debug("hostlist")
1487 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001488 for host in hosts:
1489 host['host_vars']['ansible_connection'] = 'local'
1490
1491 hosts.append(dict(
1492 name='localhost',
1493 host_vars=dict(ansible_connection='local'),
1494 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001495 return hosts
1496
James E. Blairf5dbd002015-12-23 15:26:17 -08001497
Clark Boylanb640e052014-04-03 16:41:46 -07001498class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001499 """A Gearman server for use in tests.
1500
1501 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1502 added to the queue but will not be distributed to workers
1503 until released. This attribute may be changed at any time and
1504 will take effect for subsequently enqueued jobs, but
1505 previously held jobs will still need to be explicitly
1506 released.
1507
1508 """
1509
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001510 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001511 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001512 if use_ssl:
1513 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1514 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1515 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1516 else:
1517 ssl_ca = None
1518 ssl_cert = None
1519 ssl_key = None
1520
1521 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1522 ssl_cert=ssl_cert,
1523 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001524
1525 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001526 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1527 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001528 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001529 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001530 job.waiting = self.hold_jobs_in_queue
1531 else:
1532 job.waiting = False
1533 if job.waiting:
1534 continue
1535 if job.name in connection.functions:
1536 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001537 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001538 connection.related_jobs[job.handle] = job
1539 job.worker_connection = connection
1540 job.running = True
1541 return job
1542 return None
1543
1544 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001545 """Release a held job.
1546
1547 :arg str regex: A regular expression which, if supplied, will
1548 cause only jobs with matching names to be released. If
1549 not supplied, all jobs will be released.
1550 """
Clark Boylanb640e052014-04-03 16:41:46 -07001551 released = False
1552 qlen = (len(self.high_queue) + len(self.normal_queue) +
1553 len(self.low_queue))
1554 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1555 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001556 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001557 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001558 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001559 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001560 self.log.debug("releasing queued job %s" %
1561 job.unique)
1562 job.waiting = False
1563 released = True
1564 else:
1565 self.log.debug("not releasing queued job %s" %
1566 job.unique)
1567 if released:
1568 self.wakeConnections()
1569 qlen = (len(self.high_queue) + len(self.normal_queue) +
1570 len(self.low_queue))
1571 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1572
1573
1574class FakeSMTP(object):
1575 log = logging.getLogger('zuul.FakeSMTP')
1576
1577 def __init__(self, messages, server, port):
1578 self.server = server
1579 self.port = port
1580 self.messages = messages
1581
1582 def sendmail(self, from_email, to_email, msg):
1583 self.log.info("Sending email from %s, to %s, with msg %s" % (
1584 from_email, to_email, msg))
1585
1586 headers = msg.split('\n\n', 1)[0]
1587 body = msg.split('\n\n', 1)[1]
1588
1589 self.messages.append(dict(
1590 from_email=from_email,
1591 to_email=to_email,
1592 msg=msg,
1593 headers=headers,
1594 body=body,
1595 ))
1596
1597 return True
1598
1599 def quit(self):
1600 return True
1601
1602
James E. Blairdce6cea2016-12-20 16:45:32 -08001603class FakeNodepool(object):
1604 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001605 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001606
1607 log = logging.getLogger("zuul.test.FakeNodepool")
1608
1609 def __init__(self, host, port, chroot):
1610 self.client = kazoo.client.KazooClient(
1611 hosts='%s:%s%s' % (host, port, chroot))
1612 self.client.start()
1613 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001614 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001615 self.thread = threading.Thread(target=self.run)
1616 self.thread.daemon = True
1617 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001618 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001619
1620 def stop(self):
1621 self._running = False
1622 self.thread.join()
1623 self.client.stop()
1624 self.client.close()
1625
1626 def run(self):
1627 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001628 try:
1629 self._run()
1630 except Exception:
1631 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001632 time.sleep(0.1)
1633
1634 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001635 if self.paused:
1636 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001637 for req in self.getNodeRequests():
1638 self.fulfillRequest(req)
1639
1640 def getNodeRequests(self):
1641 try:
1642 reqids = self.client.get_children(self.REQUEST_ROOT)
1643 except kazoo.exceptions.NoNodeError:
1644 return []
1645 reqs = []
1646 for oid in sorted(reqids):
1647 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001648 try:
1649 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001650 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001651 data['_oid'] = oid
1652 reqs.append(data)
1653 except kazoo.exceptions.NoNodeError:
1654 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001655 return reqs
1656
James E. Blaire18d4602017-01-05 11:17:28 -08001657 def getNodes(self):
1658 try:
1659 nodeids = self.client.get_children(self.NODE_ROOT)
1660 except kazoo.exceptions.NoNodeError:
1661 return []
1662 nodes = []
1663 for oid in sorted(nodeids):
1664 path = self.NODE_ROOT + '/' + oid
1665 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001666 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001667 data['_oid'] = oid
1668 try:
1669 lockfiles = self.client.get_children(path + '/lock')
1670 except kazoo.exceptions.NoNodeError:
1671 lockfiles = []
1672 if lockfiles:
1673 data['_lock'] = True
1674 else:
1675 data['_lock'] = False
1676 nodes.append(data)
1677 return nodes
1678
James E. Blaira38c28e2017-01-04 10:33:20 -08001679 def makeNode(self, request_id, node_type):
1680 now = time.time()
1681 path = '/nodepool/nodes/'
1682 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001683 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001684 provider='test-provider',
1685 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001686 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001687 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001688 public_ipv4='127.0.0.1',
1689 private_ipv4=None,
1690 public_ipv6=None,
1691 allocated_to=request_id,
1692 state='ready',
1693 state_time=now,
1694 created_time=now,
1695 updated_time=now,
1696 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001697 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001698 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001699 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001700 path = self.client.create(path, data,
1701 makepath=True,
1702 sequence=True)
1703 nodeid = path.split("/")[-1]
1704 return nodeid
1705
James E. Blair6ab79e02017-01-06 10:10:17 -08001706 def addFailRequest(self, request):
1707 self.fail_requests.add(request['_oid'])
1708
James E. Blairdce6cea2016-12-20 16:45:32 -08001709 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001710 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001711 return
1712 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001713 oid = request['_oid']
1714 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001715
James E. Blair6ab79e02017-01-06 10:10:17 -08001716 if oid in self.fail_requests:
1717 request['state'] = 'failed'
1718 else:
1719 request['state'] = 'fulfilled'
1720 nodes = []
1721 for node in request['node_types']:
1722 nodeid = self.makeNode(oid, node)
1723 nodes.append(nodeid)
1724 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001725
James E. Blaira38c28e2017-01-04 10:33:20 -08001726 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001727 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001728 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001729 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001730 try:
1731 self.client.set(path, data)
1732 except kazoo.exceptions.NoNodeError:
1733 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001734
1735
James E. Blair498059b2016-12-20 13:50:13 -08001736class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001737 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001738 super(ChrootedKazooFixture, self).__init__()
1739
1740 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1741 if ':' in zk_host:
1742 host, port = zk_host.split(':')
1743 else:
1744 host = zk_host
1745 port = None
1746
1747 self.zookeeper_host = host
1748
1749 if not port:
1750 self.zookeeper_port = 2181
1751 else:
1752 self.zookeeper_port = int(port)
1753
Clark Boylan621ec9a2017-04-07 17:41:33 -07001754 self.test_id = test_id
1755
James E. Blair498059b2016-12-20 13:50:13 -08001756 def _setUp(self):
1757 # Make sure the test chroot paths do not conflict
1758 random_bits = ''.join(random.choice(string.ascii_lowercase +
1759 string.ascii_uppercase)
1760 for x in range(8))
1761
Clark Boylan621ec9a2017-04-07 17:41:33 -07001762 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001763 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1764
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001765 self.addCleanup(self._cleanup)
1766
James E. Blair498059b2016-12-20 13:50:13 -08001767 # Ensure the chroot path exists and clean up any pre-existing znodes.
1768 _tmp_client = kazoo.client.KazooClient(
1769 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1770 _tmp_client.start()
1771
1772 if _tmp_client.exists(self.zookeeper_chroot):
1773 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1774
1775 _tmp_client.ensure_path(self.zookeeper_chroot)
1776 _tmp_client.stop()
1777 _tmp_client.close()
1778
James E. Blair498059b2016-12-20 13:50:13 -08001779 def _cleanup(self):
1780 '''Remove the chroot path.'''
1781 # Need a non-chroot'ed client to remove the chroot path
1782 _tmp_client = kazoo.client.KazooClient(
1783 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1784 _tmp_client.start()
1785 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1786 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001787 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001788
1789
Joshua Heskethd78b4482015-09-14 16:56:34 -06001790class MySQLSchemaFixture(fixtures.Fixture):
1791 def setUp(self):
1792 super(MySQLSchemaFixture, self).setUp()
1793
1794 random_bits = ''.join(random.choice(string.ascii_lowercase +
1795 string.ascii_uppercase)
1796 for x in range(8))
1797 self.name = '%s_%s' % (random_bits, os.getpid())
1798 self.passwd = uuid.uuid4().hex
1799 db = pymysql.connect(host="localhost",
1800 user="openstack_citest",
1801 passwd="openstack_citest",
1802 db="openstack_citest")
1803 cur = db.cursor()
1804 cur.execute("create database %s" % self.name)
1805 cur.execute(
1806 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1807 (self.name, self.name, self.passwd))
1808 cur.execute("flush privileges")
1809
1810 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1811 self.passwd,
1812 self.name)
1813 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1814 self.addCleanup(self.cleanup)
1815
1816 def cleanup(self):
1817 db = pymysql.connect(host="localhost",
1818 user="openstack_citest",
1819 passwd="openstack_citest",
1820 db="openstack_citest")
1821 cur = db.cursor()
1822 cur.execute("drop database %s" % self.name)
1823 cur.execute("drop user '%s'@'localhost'" % self.name)
1824 cur.execute("flush privileges")
1825
1826
Maru Newby3fe5f852015-01-13 04:22:14 +00001827class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001828 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001829 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001830
James E. Blair1c236df2017-02-01 14:07:24 -08001831 def attachLogs(self, *args):
1832 def reader():
1833 self._log_stream.seek(0)
1834 while True:
1835 x = self._log_stream.read(4096)
1836 if not x:
1837 break
1838 yield x.encode('utf8')
1839 content = testtools.content.content_from_reader(
1840 reader,
1841 testtools.content_type.UTF8_TEXT,
1842 False)
1843 self.addDetail('logging', content)
1844
Clark Boylanb640e052014-04-03 16:41:46 -07001845 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001846 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001847 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1848 try:
1849 test_timeout = int(test_timeout)
1850 except ValueError:
1851 # If timeout value is invalid do not set a timeout.
1852 test_timeout = 0
1853 if test_timeout > 0:
1854 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1855
1856 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1857 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1858 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1859 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1860 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1861 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1862 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1863 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1864 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1865 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001866 self._log_stream = StringIO()
1867 self.addOnException(self.attachLogs)
1868 else:
1869 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001870
James E. Blair73b41772017-05-22 13:22:55 -07001871 # NOTE(jeblair): this is temporary extra debugging to try to
1872 # track down a possible leak.
1873 orig_git_repo_init = git.Repo.__init__
1874
1875 def git_repo_init(myself, *args, **kw):
1876 orig_git_repo_init(myself, *args, **kw)
1877 self.log.debug("Created git repo 0x%x %s" %
1878 (id(myself), repr(myself)))
1879
1880 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1881 git_repo_init))
1882
James E. Blair1c236df2017-02-01 14:07:24 -08001883 handler = logging.StreamHandler(self._log_stream)
1884 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1885 '%(levelname)-8s %(message)s')
1886 handler.setFormatter(formatter)
1887
1888 logger = logging.getLogger()
1889 logger.setLevel(logging.DEBUG)
1890 logger.addHandler(handler)
1891
Clark Boylan3410d532017-04-25 12:35:29 -07001892 # Make sure we don't carry old handlers around in process state
1893 # which slows down test runs
1894 self.addCleanup(logger.removeHandler, handler)
1895 self.addCleanup(handler.close)
1896 self.addCleanup(handler.flush)
1897
James E. Blair1c236df2017-02-01 14:07:24 -08001898 # NOTE(notmorgan): Extract logging overrides for specific
1899 # libraries from the OS_LOG_DEFAULTS env and create loggers
1900 # for each. This is used to limit the output during test runs
1901 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001902 log_defaults_from_env = os.environ.get(
1903 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001904 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001905
James E. Blairdce6cea2016-12-20 16:45:32 -08001906 if log_defaults_from_env:
1907 for default in log_defaults_from_env.split(','):
1908 try:
1909 name, level_str = default.split('=', 1)
1910 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001911 logger = logging.getLogger(name)
1912 logger.setLevel(level)
1913 logger.addHandler(handler)
1914 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001915 except ValueError:
1916 # NOTE(notmorgan): Invalid format of the log default,
1917 # skip and don't try and apply a logger for the
1918 # specified module
1919 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001920
Maru Newby3fe5f852015-01-13 04:22:14 +00001921
1922class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001923 """A test case with a functioning Zuul.
1924
1925 The following class variables are used during test setup and can
1926 be overidden by subclasses but are effectively read-only once a
1927 test method starts running:
1928
1929 :cvar str config_file: This points to the main zuul config file
1930 within the fixtures directory. Subclasses may override this
1931 to obtain a different behavior.
1932
1933 :cvar str tenant_config_file: This is the tenant config file
1934 (which specifies from what git repos the configuration should
1935 be loaded). It defaults to the value specified in
1936 `config_file` but can be overidden by subclasses to obtain a
1937 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001938 configuration. See also the :py:func:`simple_layout`
1939 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001940
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001941 :cvar bool create_project_keys: Indicates whether Zuul should
1942 auto-generate keys for each project, or whether the test
1943 infrastructure should insert dummy keys to save time during
1944 startup. Defaults to False.
1945
James E. Blaire7b99a02016-08-05 14:27:34 -07001946 The following are instance variables that are useful within test
1947 methods:
1948
1949 :ivar FakeGerritConnection fake_<connection>:
1950 A :py:class:`~tests.base.FakeGerritConnection` will be
1951 instantiated for each connection present in the config file
1952 and stored here. For instance, `fake_gerrit` will hold the
1953 FakeGerritConnection object for a connection named `gerrit`.
1954
1955 :ivar FakeGearmanServer gearman_server: An instance of
1956 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1957 server that all of the Zuul components in this test use to
1958 communicate with each other.
1959
Paul Belanger174a8272017-03-14 13:20:10 -04001960 :ivar RecordingExecutorServer executor_server: An instance of
1961 :py:class:`~tests.base.RecordingExecutorServer` which is the
1962 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001963
1964 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1965 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001966 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001967 list upon completion.
1968
1969 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1970 objects representing completed builds. They are appended to
1971 the list in the order they complete.
1972
1973 """
1974
James E. Blair83005782015-12-11 14:46:03 -08001975 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001976 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001977 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001978 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001979
1980 def _startMerger(self):
1981 self.merge_server = zuul.merger.server.MergeServer(self.config,
1982 self.connections)
1983 self.merge_server.start()
1984
Maru Newby3fe5f852015-01-13 04:22:14 +00001985 def setUp(self):
1986 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001987
1988 self.setupZK()
1989
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001990 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001991 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001992 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1993 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001994 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001995 tmp_root = tempfile.mkdtemp(
1996 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001997 self.test_root = os.path.join(tmp_root, "zuul-test")
1998 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001999 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002000 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002001 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002002 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2003 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002004
2005 if os.path.exists(self.test_root):
2006 shutil.rmtree(self.test_root)
2007 os.makedirs(self.test_root)
2008 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002009 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002010 os.makedirs(self.merger_state_root)
2011 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002012
2013 # Make per test copy of Configuration.
2014 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002015 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2016 if not os.path.exists(self.private_key_file):
2017 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2018 shutil.copy(src_private_key_file, self.private_key_file)
2019 shutil.copy('{}.pub'.format(src_private_key_file),
2020 '{}.pub'.format(self.private_key_file))
2021 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002022 self.config.set('scheduler', 'tenant_config',
2023 os.path.join(
2024 FIXTURE_DIR,
2025 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002026 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002027 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002028 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002029 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002030 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002031
Clark Boylanb640e052014-04-03 16:41:46 -07002032 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10002033 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
2034 # see: https://github.com/jsocol/pystatsd/issues/61
2035 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07002036 os.environ['STATSD_PORT'] = str(self.statsd.port)
2037 self.statsd.start()
2038 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05002039 importlib.reload(statsd)
2040 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07002041
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002042 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002043
2044 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002045 self.log.info("Gearman server on port %s" %
2046 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002047 if self.use_ssl:
2048 self.log.info('SSL enabled for gearman')
2049 self.config.set(
2050 'gearman', 'ssl_ca',
2051 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2052 self.config.set(
2053 'gearman', 'ssl_cert',
2054 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2055 self.config.set(
2056 'gearman', 'ssl_key',
2057 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002058
James E. Blaire511d2f2016-12-08 15:22:26 -08002059 gerritsource.GerritSource.replication_timeout = 1.5
2060 gerritsource.GerritSource.replication_retry_interval = 0.5
2061 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002062
Joshua Hesketh352264b2015-08-11 23:42:08 +10002063 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07002064
Jan Hruban7083edd2015-08-21 14:00:54 +02002065 self.webapp = zuul.webapp.WebApp(
2066 self.sched, port=0, listen_address='127.0.0.1')
2067
Jan Hruban6b71aff2015-10-22 16:58:08 +02002068 self.event_queues = [
2069 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002070 self.sched.trigger_event_queue,
2071 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002072 ]
2073
James E. Blairfef78942016-03-11 16:28:56 -08002074 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002075 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002076
Paul Belanger174a8272017-03-14 13:20:10 -04002077 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002078 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002079 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002080 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002081 _test_root=self.test_root,
2082 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002083 self.executor_server.start()
2084 self.history = self.executor_server.build_history
2085 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002086
Paul Belanger174a8272017-03-14 13:20:10 -04002087 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002088 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002089 self.merge_client = zuul.merger.client.MergeClient(
2090 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002091 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002092 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002093 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002094
James E. Blair0d5a36e2017-02-21 10:53:44 -05002095 self.fake_nodepool = FakeNodepool(
2096 self.zk_chroot_fixture.zookeeper_host,
2097 self.zk_chroot_fixture.zookeeper_port,
2098 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002099
Paul Belanger174a8272017-03-14 13:20:10 -04002100 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002101 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002102 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002103 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002104
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002105 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002106
2107 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002108 self.webapp.start()
2109 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002110 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002111 # Cleanups are run in reverse order
2112 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002113 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002114 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002115
James E. Blairb9c0d772017-03-03 14:34:49 -08002116 self.sched.reconfigure(self.config)
2117 self.sched.resume()
2118
Tobias Henkel7df274b2017-05-26 17:41:11 +02002119 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002120 # Set up gerrit related fakes
2121 # Set a changes database so multiple FakeGerrit's can report back to
2122 # a virtual canonical database given by the configured hostname
2123 self.gerrit_changes_dbs = {}
2124
2125 def getGerritConnection(driver, name, config):
2126 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2127 con = FakeGerritConnection(driver, name, config,
2128 changes_db=db,
2129 upstream_root=self.upstream_root)
2130 self.event_queues.append(con.event_queue)
2131 setattr(self, 'fake_' + name, con)
2132 return con
2133
2134 self.useFixture(fixtures.MonkeyPatch(
2135 'zuul.driver.gerrit.GerritDriver.getConnection',
2136 getGerritConnection))
2137
Gregory Haynes4fc12542015-04-22 20:38:06 -07002138 def getGithubConnection(driver, name, config):
2139 con = FakeGithubConnection(driver, name, config,
2140 upstream_root=self.upstream_root)
2141 setattr(self, 'fake_' + name, con)
2142 return con
2143
2144 self.useFixture(fixtures.MonkeyPatch(
2145 'zuul.driver.github.GithubDriver.getConnection',
2146 getGithubConnection))
2147
James E. Blaire511d2f2016-12-08 15:22:26 -08002148 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002149 # TODO(jhesketh): This should come from lib.connections for better
2150 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002151 # Register connections from the config
2152 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002153
Joshua Hesketh352264b2015-08-11 23:42:08 +10002154 def FakeSMTPFactory(*args, **kw):
2155 args = [self.smtp_messages] + list(args)
2156 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002157
Joshua Hesketh352264b2015-08-11 23:42:08 +10002158 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002159
James E. Blaire511d2f2016-12-08 15:22:26 -08002160 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002161 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002162 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002163
James E. Blair83005782015-12-11 14:46:03 -08002164 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002165 # This creates the per-test configuration object. It can be
2166 # overriden by subclasses, but should not need to be since it
2167 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002168 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002169 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002170
James E. Blair39840362017-06-23 20:34:02 +01002171 sections = ['zuul', 'scheduler', 'executor', 'merger']
2172 for section in sections:
2173 if not self.config.has_section(section):
2174 self.config.add_section(section)
2175
James E. Blair06cc3922017-04-19 10:08:10 -07002176 if not self.setupSimpleLayout():
2177 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002178 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002179 self.tenant_config_file)
2180 git_path = os.path.join(
2181 os.path.dirname(
2182 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2183 'git')
2184 if os.path.exists(git_path):
2185 for reponame in os.listdir(git_path):
2186 project = reponame.replace('_', '/')
2187 self.copyDirToRepo(project,
2188 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002189 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002190 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002191 self.setupAllProjectKeys()
2192
James E. Blair06cc3922017-04-19 10:08:10 -07002193 def setupSimpleLayout(self):
2194 # If the test method has been decorated with a simple_layout,
2195 # use that instead of the class tenant_config_file. Set up a
2196 # single config-project with the specified layout, and
2197 # initialize repos for all of the 'project' entries which
2198 # appear in the layout.
2199 test_name = self.id().split('.')[-1]
2200 test = getattr(self, test_name)
2201 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002202 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002203 else:
2204 return False
2205
James E. Blairb70e55a2017-04-19 12:57:02 -07002206 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002207 path = os.path.join(FIXTURE_DIR, path)
2208 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002209 data = f.read()
2210 layout = yaml.safe_load(data)
2211 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002212 untrusted_projects = []
2213 for item in layout:
2214 if 'project' in item:
2215 name = item['project']['name']
2216 untrusted_projects.append(name)
2217 self.init_repo(name)
2218 self.addCommitToRepo(name, 'initial commit',
2219 files={'README': ''},
2220 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002221 if 'job' in item:
2222 jobname = item['job']['name']
2223 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002224
2225 root = os.path.join(self.test_root, "config")
2226 if not os.path.exists(root):
2227 os.makedirs(root)
2228 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2229 config = [{'tenant':
2230 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002231 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002232 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002233 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002234 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002235 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002236 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002237 os.path.join(FIXTURE_DIR, f.name))
2238
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002239 self.init_repo('org/common-config')
2240 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002241 files, branch='master', tag='init')
2242
2243 return True
2244
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002245 def setupAllProjectKeys(self):
2246 if self.create_project_keys:
2247 return
2248
James E. Blair39840362017-06-23 20:34:02 +01002249 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002250 with open(os.path.join(FIXTURE_DIR, path)) as f:
2251 tenant_config = yaml.safe_load(f.read())
2252 for tenant in tenant_config:
2253 sources = tenant['tenant']['source']
2254 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002255 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002256 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002257 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002258 self.setupProjectKeys(source, project)
2259
2260 def setupProjectKeys(self, source, project):
2261 # Make sure we set up an RSA key for the project so that we
2262 # don't spend time generating one:
2263
James E. Blair6459db12017-06-29 14:57:20 -07002264 if isinstance(project, dict):
2265 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002266 key_root = os.path.join(self.state_root, 'keys')
2267 if not os.path.isdir(key_root):
2268 os.mkdir(key_root, 0o700)
2269 private_key_file = os.path.join(key_root, source, project + '.pem')
2270 private_key_dir = os.path.dirname(private_key_file)
2271 self.log.debug("Installing test keys for project %s at %s" % (
2272 project, private_key_file))
2273 if not os.path.isdir(private_key_dir):
2274 os.makedirs(private_key_dir)
2275 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2276 with open(private_key_file, 'w') as o:
2277 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002278
James E. Blair498059b2016-12-20 13:50:13 -08002279 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002280 self.zk_chroot_fixture = self.useFixture(
2281 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002282 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002283 self.zk_chroot_fixture.zookeeper_host,
2284 self.zk_chroot_fixture.zookeeper_port,
2285 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002286
James E. Blair96c6bf82016-01-15 16:20:40 -08002287 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002288 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002289
2290 files = {}
2291 for (dirpath, dirnames, filenames) in os.walk(source_path):
2292 for filename in filenames:
2293 test_tree_filepath = os.path.join(dirpath, filename)
2294 common_path = os.path.commonprefix([test_tree_filepath,
2295 source_path])
2296 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2297 with open(test_tree_filepath, 'r') as f:
2298 content = f.read()
2299 files[relative_filepath] = content
2300 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002301 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002302
James E. Blaire18d4602017-01-05 11:17:28 -08002303 def assertNodepoolState(self):
2304 # Make sure that there are no pending requests
2305
2306 requests = self.fake_nodepool.getNodeRequests()
2307 self.assertEqual(len(requests), 0)
2308
2309 nodes = self.fake_nodepool.getNodes()
2310 for node in nodes:
2311 self.assertFalse(node['_lock'], "Node %s is locked" %
2312 (node['_oid'],))
2313
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002314 def assertNoGeneratedKeys(self):
2315 # Make sure that Zuul did not generate any project keys
2316 # (unless it was supposed to).
2317
2318 if self.create_project_keys:
2319 return
2320
2321 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2322 test_key = i.read()
2323
2324 key_root = os.path.join(self.state_root, 'keys')
2325 for root, dirname, files in os.walk(key_root):
2326 for fn in files:
2327 with open(os.path.join(root, fn)) as f:
2328 self.assertEqual(test_key, f.read())
2329
Clark Boylanb640e052014-04-03 16:41:46 -07002330 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002331 self.log.debug("Assert final state")
2332 # Make sure no jobs are running
2333 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002334 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002335 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002336 gc.collect()
2337 for obj in gc.get_objects():
2338 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002339 self.log.debug("Leaked git repo object: 0x%x %s" %
2340 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002341 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002342 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002343 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002344 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002345 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002346 for tenant in self.sched.abide.tenants.values():
2347 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002348 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002349 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002350
2351 def shutdown(self):
2352 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002353 self.executor_server.hold_jobs_in_build = False
2354 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002355 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002356 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002357 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002358 self.sched.stop()
2359 self.sched.join()
2360 self.statsd.stop()
2361 self.statsd.join()
2362 self.webapp.stop()
2363 self.webapp.join()
2364 self.rpc.stop()
2365 self.rpc.join()
2366 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002367 self.fake_nodepool.stop()
2368 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002369 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002370 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002371 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002372 # Further the pydevd threads also need to be whitelisted so debugging
2373 # e.g. in PyCharm is possible without breaking shutdown.
2374 whitelist = ['executor-watchdog',
2375 'pydevd.CommandThread',
2376 'pydevd.Reader',
2377 'pydevd.Writer',
2378 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002379 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002380 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002381 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002382 log_str = ""
2383 for thread_id, stack_frame in sys._current_frames().items():
2384 log_str += "Thread: %s\n" % thread_id
2385 log_str += "".join(traceback.format_stack(stack_frame))
2386 self.log.debug(log_str)
2387 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002388
James E. Blaira002b032017-04-18 10:35:48 -07002389 def assertCleanShutdown(self):
2390 pass
2391
James E. Blairc4ba97a2017-04-19 16:26:24 -07002392 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002393 parts = project.split('/')
2394 path = os.path.join(self.upstream_root, *parts[:-1])
2395 if not os.path.exists(path):
2396 os.makedirs(path)
2397 path = os.path.join(self.upstream_root, project)
2398 repo = git.Repo.init(path)
2399
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002400 with repo.config_writer() as config_writer:
2401 config_writer.set_value('user', 'email', 'user@example.com')
2402 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002403
Clark Boylanb640e052014-04-03 16:41:46 -07002404 repo.index.commit('initial commit')
2405 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002406 if tag:
2407 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002408
James E. Blair97d902e2014-08-21 13:25:56 -07002409 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002410 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002411 repo.git.clean('-x', '-f', '-d')
2412
James E. Blair97d902e2014-08-21 13:25:56 -07002413 def create_branch(self, project, branch):
2414 path = os.path.join(self.upstream_root, project)
2415 repo = git.Repo.init(path)
2416 fn = os.path.join(path, 'README')
2417
2418 branch_head = repo.create_head(branch)
2419 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002420 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002421 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002422 f.close()
2423 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002424 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002425
James E. Blair97d902e2014-08-21 13:25:56 -07002426 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002427 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002428 repo.git.clean('-x', '-f', '-d')
2429
Sachi King9f16d522016-03-16 12:20:45 +11002430 def create_commit(self, project):
2431 path = os.path.join(self.upstream_root, project)
2432 repo = git.Repo(path)
2433 repo.head.reference = repo.heads['master']
2434 file_name = os.path.join(path, 'README')
2435 with open(file_name, 'a') as f:
2436 f.write('creating fake commit\n')
2437 repo.index.add([file_name])
2438 commit = repo.index.commit('Creating a fake commit')
2439 return commit.hexsha
2440
James E. Blairf4a5f022017-04-18 14:01:10 -07002441 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002442 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002443 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002444 while len(self.builds):
2445 self.release(self.builds[0])
2446 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002447 i += 1
2448 if count is not None and i >= count:
2449 break
James E. Blairb8c16472015-05-05 14:55:26 -07002450
James E. Blairdf25ddc2017-07-08 07:57:09 -07002451 def getSortedBuilds(self):
2452 "Return the list of currently running builds sorted by name"
2453
2454 return sorted(self.builds, key=lambda x: x.name)
2455
Clark Boylanb640e052014-04-03 16:41:46 -07002456 def release(self, job):
2457 if isinstance(job, FakeBuild):
2458 job.release()
2459 else:
2460 job.waiting = False
2461 self.log.debug("Queued job %s released" % job.unique)
2462 self.gearman_server.wakeConnections()
2463
2464 def getParameter(self, job, name):
2465 if isinstance(job, FakeBuild):
2466 return job.parameters[name]
2467 else:
2468 parameters = json.loads(job.arguments)
2469 return parameters[name]
2470
Clark Boylanb640e052014-04-03 16:41:46 -07002471 def haveAllBuildsReported(self):
2472 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002473 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002474 return False
2475 # Find out if every build that the worker has completed has been
2476 # reported back to Zuul. If it hasn't then that means a Gearman
2477 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002478 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002479 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002480 if not zbuild:
2481 # It has already been reported
2482 continue
2483 # It hasn't been reported yet.
2484 return False
2485 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002486 worker = self.executor_server.executor_worker
2487 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002488 if connection.state == 'GRAB_WAIT':
2489 return False
2490 return True
2491
2492 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002493 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002494 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002495 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002496 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002497 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002498 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002499 for j in conn.related_jobs.values():
2500 if j.unique == build.uuid:
2501 client_job = j
2502 break
2503 if not client_job:
2504 self.log.debug("%s is not known to the gearman client" %
2505 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002506 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002507 if not client_job.handle:
2508 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002509 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002510 server_job = self.gearman_server.jobs.get(client_job.handle)
2511 if not server_job:
2512 self.log.debug("%s is not known to the gearman server" %
2513 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002514 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002515 if not hasattr(server_job, 'waiting'):
2516 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002517 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002518 if server_job.waiting:
2519 continue
James E. Blair17302972016-08-10 16:11:42 -07002520 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002521 self.log.debug("%s has not reported start" % build)
2522 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002523 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002524 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002525 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002526 if worker_build:
2527 if worker_build.isWaiting():
2528 continue
2529 else:
2530 self.log.debug("%s is running" % worker_build)
2531 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002532 else:
James E. Blair962220f2016-08-03 11:22:38 -07002533 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002534 return False
James E. Blaira002b032017-04-18 10:35:48 -07002535 for (build_uuid, job_worker) in \
2536 self.executor_server.job_workers.items():
2537 if build_uuid not in seen_builds:
2538 self.log.debug("%s is not finalized" % build_uuid)
2539 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002540 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002541
James E. Blairdce6cea2016-12-20 16:45:32 -08002542 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002543 if self.fake_nodepool.paused:
2544 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002545 if self.sched.nodepool.requests:
2546 return False
2547 return True
2548
Jan Hruban6b71aff2015-10-22 16:58:08 +02002549 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002550 for event_queue in self.event_queues:
2551 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002552
2553 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002554 for event_queue in self.event_queues:
2555 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002556
Clark Boylanb640e052014-04-03 16:41:46 -07002557 def waitUntilSettled(self):
2558 self.log.debug("Waiting until settled...")
2559 start = time.time()
2560 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002561 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002562 self.log.error("Timeout waiting for Zuul to settle")
2563 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002564 for event_queue in self.event_queues:
2565 self.log.error(" %s: %s" %
2566 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002567 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002568 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002569 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002570 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002571 self.log.error("All requests completed: %s" %
2572 (self.areAllNodeRequestsComplete(),))
2573 self.log.error("Merge client jobs: %s" %
2574 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002575 raise Exception("Timeout waiting for Zuul to settle")
2576 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002577
Paul Belanger174a8272017-03-14 13:20:10 -04002578 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002579 # have all build states propogated to zuul?
2580 if self.haveAllBuildsReported():
2581 # Join ensures that the queue is empty _and_ events have been
2582 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002583 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002584 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002585 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002586 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002587 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002588 self.areAllNodeRequestsComplete() and
2589 all(self.eventQueuesEmpty())):
2590 # The queue empty check is placed at the end to
2591 # ensure that if a component adds an event between
2592 # when locked the run handler and checked that the
2593 # components were stable, we don't erroneously
2594 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002595 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002596 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002597 self.log.debug("...settled.")
2598 return
2599 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002600 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002601 self.sched.wake_event.wait(0.1)
2602
2603 def countJobResults(self, jobs, result):
2604 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002605 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002606
Monty Taylor0d926122017-05-24 08:07:56 -05002607 def getBuildByName(self, name):
2608 for build in self.builds:
2609 if build.name == name:
2610 return build
2611 raise Exception("Unable to find build %s" % name)
2612
James E. Blair96c6bf82016-01-15 16:20:40 -08002613 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002614 for job in self.history:
2615 if (job.name == name and
2616 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002617 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002618 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002619 raise Exception("Unable to find job %s in history" % name)
2620
2621 def assertEmptyQueues(self):
2622 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002623 for tenant in self.sched.abide.tenants.values():
2624 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002625 for pipeline_queue in pipeline.queues:
2626 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002627 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002628 pipeline.name, pipeline_queue.name,
2629 pipeline_queue.queue))
2630 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002631 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002632
2633 def assertReportedStat(self, key, value=None, kind=None):
2634 start = time.time()
2635 while time.time() < (start + 5):
2636 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002637 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002638 if key == k:
2639 if value is None and kind is None:
2640 return
2641 elif value:
2642 if value == v:
2643 return
2644 elif kind:
2645 if v.endswith('|' + kind):
2646 return
2647 time.sleep(0.1)
2648
Clark Boylanb640e052014-04-03 16:41:46 -07002649 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002650
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002651 def assertBuilds(self, builds):
2652 """Assert that the running builds are as described.
2653
2654 The list of running builds is examined and must match exactly
2655 the list of builds described by the input.
2656
2657 :arg list builds: A list of dictionaries. Each item in the
2658 list must match the corresponding build in the build
2659 history, and each element of the dictionary must match the
2660 corresponding attribute of the build.
2661
2662 """
James E. Blair3158e282016-08-19 09:34:11 -07002663 try:
2664 self.assertEqual(len(self.builds), len(builds))
2665 for i, d in enumerate(builds):
2666 for k, v in d.items():
2667 self.assertEqual(
2668 getattr(self.builds[i], k), v,
2669 "Element %i in builds does not match" % (i,))
2670 except Exception:
2671 for build in self.builds:
2672 self.log.error("Running build: %s" % build)
2673 else:
2674 self.log.error("No running builds")
2675 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002676
James E. Blairb536ecc2016-08-31 10:11:42 -07002677 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002678 """Assert that the completed builds are as described.
2679
2680 The list of completed builds is examined and must match
2681 exactly the list of builds described by the input.
2682
2683 :arg list history: A list of dictionaries. Each item in the
2684 list must match the corresponding build in the build
2685 history, and each element of the dictionary must match the
2686 corresponding attribute of the build.
2687
James E. Blairb536ecc2016-08-31 10:11:42 -07002688 :arg bool ordered: If true, the history must match the order
2689 supplied, if false, the builds are permitted to have
2690 arrived in any order.
2691
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002692 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002693 def matches(history_item, item):
2694 for k, v in item.items():
2695 if getattr(history_item, k) != v:
2696 return False
2697 return True
James E. Blair3158e282016-08-19 09:34:11 -07002698 try:
2699 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002700 if ordered:
2701 for i, d in enumerate(history):
2702 if not matches(self.history[i], d):
2703 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002704 "Element %i in history does not match %s" %
2705 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002706 else:
2707 unseen = self.history[:]
2708 for i, d in enumerate(history):
2709 found = False
2710 for unseen_item in unseen:
2711 if matches(unseen_item, d):
2712 found = True
2713 unseen.remove(unseen_item)
2714 break
2715 if not found:
2716 raise Exception("No match found for element %i "
2717 "in history" % (i,))
2718 if unseen:
2719 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002720 except Exception:
2721 for build in self.history:
2722 self.log.error("Completed build: %s" % build)
2723 else:
2724 self.log.error("No completed builds")
2725 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002726
James E. Blair6ac368c2016-12-22 18:07:20 -08002727 def printHistory(self):
2728 """Log the build history.
2729
2730 This can be useful during tests to summarize what jobs have
2731 completed.
2732
2733 """
2734 self.log.debug("Build history:")
2735 for build in self.history:
2736 self.log.debug(build)
2737
James E. Blair59fdbac2015-12-07 17:08:06 -08002738 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002739 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2740
James E. Blair9ea70072017-04-19 16:05:30 -07002741 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002742 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002743 if not os.path.exists(root):
2744 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002745 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2746 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002747- tenant:
2748 name: openstack
2749 source:
2750 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002751 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002752 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002753 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002754 - org/project
2755 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002756 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002757 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002758 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002759 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002760 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002761
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002762 def addCommitToRepo(self, project, message, files,
2763 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002764 path = os.path.join(self.upstream_root, project)
2765 repo = git.Repo(path)
2766 repo.head.reference = branch
2767 zuul.merger.merger.reset_repo_to_head(repo)
2768 for fn, content in files.items():
2769 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002770 try:
2771 os.makedirs(os.path.dirname(fn))
2772 except OSError:
2773 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002774 with open(fn, 'w') as f:
2775 f.write(content)
2776 repo.index.add([fn])
2777 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002778 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002779 repo.heads[branch].commit = commit
2780 repo.head.reference = branch
2781 repo.git.clean('-x', '-f', '-d')
2782 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002783 if tag:
2784 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002785 return before
2786
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002787 def commitConfigUpdate(self, project_name, source_name):
2788 """Commit an update to zuul.yaml
2789
2790 This overwrites the zuul.yaml in the specificed project with
2791 the contents specified.
2792
2793 :arg str project_name: The name of the project containing
2794 zuul.yaml (e.g., common-config)
2795
2796 :arg str source_name: The path to the file (underneath the
2797 test fixture directory) whose contents should be used to
2798 replace zuul.yaml.
2799 """
2800
2801 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002802 files = {}
2803 with open(source_path, 'r') as f:
2804 data = f.read()
2805 layout = yaml.safe_load(data)
2806 files['zuul.yaml'] = data
2807 for item in layout:
2808 if 'job' in item:
2809 jobname = item['job']['name']
2810 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002811 before = self.addCommitToRepo(
2812 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002813 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002814 return before
2815
Clint Byrum627ba362017-08-14 13:20:40 -07002816 def newTenantConfig(self, source_name):
2817 """ Use this to update the tenant config file in tests
2818
2819 This will update self.tenant_config_file to point to a temporary file
2820 for the duration of this particular test. The content of that file will
2821 be taken from FIXTURE_DIR/source_name
2822
2823 After the test the original value of self.tenant_config_file will be
2824 restored.
2825
2826 :arg str source_name: The path of the file under
2827 FIXTURE_DIR that will be used to populate the new tenant
2828 config file.
2829 """
2830 source_path = os.path.join(FIXTURE_DIR, source_name)
2831 orig_tenant_config_file = self.tenant_config_file
2832 with tempfile.NamedTemporaryFile(
2833 delete=False, mode='wb') as new_tenant_config:
2834 self.tenant_config_file = new_tenant_config.name
2835 with open(source_path, mode='rb') as source_tenant_config:
2836 new_tenant_config.write(source_tenant_config.read())
2837 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2838 self.setupAllProjectKeys()
2839 self.log.debug(
2840 'tenant_config_file = {}'.format(self.tenant_config_file))
2841
2842 def _restoreTenantConfig():
2843 self.log.debug(
2844 'restoring tenant_config_file = {}'.format(
2845 orig_tenant_config_file))
2846 os.unlink(self.tenant_config_file)
2847 self.tenant_config_file = orig_tenant_config_file
2848 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2849 self.addCleanup(_restoreTenantConfig)
2850
James E. Blair7fc8daa2016-08-08 15:37:15 -07002851 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002852
James E. Blair7fc8daa2016-08-08 15:37:15 -07002853 """Inject a Fake (Gerrit) event.
2854
2855 This method accepts a JSON-encoded event and simulates Zuul
2856 having received it from Gerrit. It could (and should)
2857 eventually apply to any connection type, but is currently only
2858 used with Gerrit connections. The name of the connection is
2859 used to look up the corresponding server, and the event is
2860 simulated as having been received by all Zuul connections
2861 attached to that server. So if two Gerrit connections in Zuul
2862 are connected to the same Gerrit server, and you invoke this
2863 method specifying the name of one of them, the event will be
2864 received by both.
2865
2866 .. note::
2867
2868 "self.fake_gerrit.addEvent" calls should be migrated to
2869 this method.
2870
2871 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002872 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002873 :arg str event: The JSON-encoded event.
2874
2875 """
2876 specified_conn = self.connections.connections[connection]
2877 for conn in self.connections.connections.values():
2878 if (isinstance(conn, specified_conn.__class__) and
2879 specified_conn.server == conn.server):
2880 conn.addEvent(event)
2881
James E. Blaird8af5422017-05-24 13:59:40 -07002882 def getUpstreamRepos(self, projects):
2883 """Return upstream git repo objects for the listed projects
2884
2885 :arg list projects: A list of strings, each the canonical name
2886 of a project.
2887
2888 :returns: A dictionary of {name: repo} for every listed
2889 project.
2890 :rtype: dict
2891
2892 """
2893
2894 repos = {}
2895 for project in projects:
2896 # FIXME(jeblair): the upstream root does not yet have a
2897 # hostname component; that needs to be added, and this
2898 # line removed:
2899 tmp_project_name = '/'.join(project.split('/')[1:])
2900 path = os.path.join(self.upstream_root, tmp_project_name)
2901 repo = git.Repo(path)
2902 repos[project] = repo
2903 return repos
2904
James E. Blair3f876d52016-07-22 13:07:14 -07002905
2906class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002907 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002908 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002909
Jamie Lennox7655b552017-03-17 12:33:38 +11002910 @contextmanager
2911 def jobLog(self, build):
2912 """Print job logs on assertion errors
2913
2914 This method is a context manager which, if it encounters an
2915 ecxeption, adds the build log to the debug output.
2916
2917 :arg Build build: The build that's being asserted.
2918 """
2919 try:
2920 yield
2921 except Exception:
2922 path = os.path.join(self.test_root, build.uuid,
2923 'work', 'logs', 'job-output.txt')
2924 with open(path) as f:
2925 self.log.debug(f.read())
2926 raise
2927
Joshua Heskethd78b4482015-09-14 16:56:34 -06002928
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002929class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002930 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002931 use_ssl = True
2932
2933
Joshua Heskethd78b4482015-09-14 16:56:34 -06002934class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002935 def setup_config(self):
2936 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002937 for section_name in self.config.sections():
2938 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2939 section_name, re.I)
2940 if not con_match:
2941 continue
2942
2943 if self.config.get(section_name, 'driver') == 'sql':
2944 f = MySQLSchemaFixture()
2945 self.useFixture(f)
2946 if (self.config.get(section_name, 'dburi') ==
2947 '$MYSQL_FIXTURE_DBURI$'):
2948 self.config.set(section_name, 'dburi', f.dburi)