blob: b14491c3bdd203c93f981091bfb4763b0b43724e [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
Clark Boylanb640e052014-04-03 16:41:46 -0700484 def review(self, project, changeid, message, action):
485 number, ps = changeid.split(',')
486 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000487
488 # Add the approval back onto the change (ie simulate what gerrit would
489 # do).
490 # Usually when zuul leaves a review it'll create a feedback loop where
491 # zuul's review enters another gerrit event (which is then picked up by
492 # zuul). However, we can't mimic this behaviour (by adding this
493 # approval event into the queue) as it stops jobs from checking what
494 # happens before this event is triggered. If a job needs to see what
495 # happens they can add their own verified event into the queue.
496 # Nevertheless, we can update change with the new review in gerrit.
497
James E. Blair8b5408c2016-08-08 15:37:46 -0700498 for cat in action.keys():
499 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000500 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000501
Clark Boylanb640e052014-04-03 16:41:46 -0700502 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000503
Clark Boylanb640e052014-04-03 16:41:46 -0700504 if 'submit' in action:
505 change.setMerged()
506 if message:
507 change.setReported()
508
509 def query(self, number):
510 change = self.changes.get(int(number))
511 if change:
512 return change.query()
513 return {}
514
James E. Blairc494d542014-08-06 09:23:52 -0700515 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700516 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700517 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800518 if query.startswith('change:'):
519 # Query a specific changeid
520 changeid = query[len('change:'):]
521 l = [change.query() for change in self.changes.values()
522 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700523 elif query.startswith('message:'):
524 # Query the content of a commit message
525 msg = query[len('message:'):].strip()
526 l = [change.query() for change in self.changes.values()
527 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800528 else:
529 # Query all open changes
530 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700531 return l
James E. Blairc494d542014-08-06 09:23:52 -0700532
Joshua Hesketh352264b2015-08-11 23:42:08 +1000533 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700534 pass
535
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200536 def _uploadPack(self, project):
537 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
538 'multi_ack thin-pack side-band side-band-64k ofs-delta '
539 'shallow no-progress include-tag multi_ack_detailed no-done\n')
540 path = os.path.join(self.upstream_root, project.name)
541 repo = git.Repo(path)
542 for ref in repo.refs:
543 r = ref.object.hexsha + ' ' + ref.path + '\n'
544 ret += '%04x%s' % (len(r) + 4, r)
545 ret += '0000'
546 return ret
547
Joshua Hesketh352264b2015-08-11 23:42:08 +1000548 def getGitUrl(self, project):
549 return os.path.join(self.upstream_root, project.name)
550
Clark Boylanb640e052014-04-03 16:41:46 -0700551
Gregory Haynes4fc12542015-04-22 20:38:06 -0700552class GithubChangeReference(git.Reference):
553 _common_path_default = "refs/pull"
554 _points_to_commits_only = True
555
556
Tobias Henkel64e37a02017-08-02 10:13:30 +0200557class FakeGithub(object):
558
559 class FakeUser(object):
560 def __init__(self, login):
561 self.login = login
562 self.name = "Github User"
563 self.email = "github.user@example.com"
564
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200565 class FakeBranch(object):
566 def __init__(self, branch='master'):
567 self.name = branch
568
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200569 class FakeStatus(object):
570 def __init__(self, state, url, description, context, user):
571 self._state = state
572 self._url = url
573 self._description = description
574 self._context = context
575 self._user = user
576
577 def as_dict(self):
578 return {
579 'state': self._state,
580 'url': self._url,
581 'description': self._description,
582 'context': self._context,
583 'creator': {
584 'login': self._user
585 }
586 }
587
588 class FakeCommit(object):
589 def __init__(self):
590 self._statuses = []
591
592 def set_status(self, state, url, description, context, user):
593 status = FakeGithub.FakeStatus(
594 state, url, description, context, user)
595 # always insert a status to the front of the list, to represent
596 # the last status provided for a commit.
597 self._statuses.insert(0, status)
598
599 def statuses(self):
600 return self._statuses
601
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200602 class FakeRepository(object):
603 def __init__(self):
604 self._branches = [FakeGithub.FakeBranch()]
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200605 self._commits = {}
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200606
Tobias Henkeleca46202017-08-02 20:27:10 +0200607 def branches(self, protected=False):
608 if protected:
609 # simulate there is no protected branch
610 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200611 return self._branches
612
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200613 def create_status(self, sha, state, url, description, context,
614 user='zuul'):
615 # Since we're bypassing github API, which would require a user, we
616 # default the user as 'zuul' here.
617 commit = self._commits.get(sha, None)
618 if commit is None:
619 commit = FakeGithub.FakeCommit()
620 self._commits[sha] = commit
621 commit.set_status(state, url, description, context, user)
622
623 def commit(self, sha):
624 commit = self._commits.get(sha, None)
625 if commit is None:
626 commit = FakeGithub.FakeCommit()
627 self._commits[sha] = commit
628 return commit
629
630 def __init__(self):
631 self._repos = {}
632
Tobias Henkel64e37a02017-08-02 10:13:30 +0200633 def user(self, login):
634 return self.FakeUser(login)
635
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200636 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200637 return self._repos.get((owner, proj), None)
638
639 def repo_from_project(self, project):
640 # This is a convenience method for the tests.
641 owner, proj = project.split('/')
642 return self.repository(owner, proj)
643
644 def addProject(self, project):
645 owner, proj = project.name.split('/')
646 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200647
Tobias Henkel64e37a02017-08-02 10:13:30 +0200648
Gregory Haynes4fc12542015-04-22 20:38:06 -0700649class FakeGithubPullRequest(object):
650
651 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800652 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700653 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700654 """Creates a new PR with several commits.
655 Sends an event about opened PR."""
656 self.github = github
657 self.source = github
658 self.number = number
659 self.project = project
660 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100661 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700662 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100663 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700664 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100665 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700666 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100667 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100668 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800669 self.reviews = []
670 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700671 self.updated_at = None
672 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100673 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100674 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700675 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700676 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100677 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700678 self._updateTimeStamp()
679
Jan Hruban570d01c2016-03-10 21:51:32 +0100680 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700681 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100682 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700683 self._updateTimeStamp()
684
Jan Hruban570d01c2016-03-10 21:51:32 +0100685 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700686 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100687 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700688 self._updateTimeStamp()
689
690 def getPullRequestOpenedEvent(self):
691 return self._getPullRequestEvent('opened')
692
693 def getPullRequestSynchronizeEvent(self):
694 return self._getPullRequestEvent('synchronize')
695
696 def getPullRequestReopenedEvent(self):
697 return self._getPullRequestEvent('reopened')
698
699 def getPullRequestClosedEvent(self):
700 return self._getPullRequestEvent('closed')
701
Jesse Keatinga41566f2017-06-14 18:17:51 -0700702 def getPullRequestEditedEvent(self):
703 return self._getPullRequestEvent('edited')
704
Gregory Haynes4fc12542015-04-22 20:38:06 -0700705 def addComment(self, message):
706 self.comments.append(message)
707 self._updateTimeStamp()
708
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200709 def getCommentAddedEvent(self, text):
710 name = 'issue_comment'
711 data = {
712 'action': 'created',
713 'issue': {
714 'number': self.number
715 },
716 'comment': {
717 'body': text
718 },
719 'repository': {
720 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100721 },
722 'sender': {
723 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200724 }
725 }
726 return (name, data)
727
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800728 def getReviewAddedEvent(self, review):
729 name = 'pull_request_review'
730 data = {
731 'action': 'submitted',
732 'pull_request': {
733 'number': self.number,
734 'title': self.subject,
735 'updated_at': self.updated_at,
736 'base': {
737 'ref': self.branch,
738 'repo': {
739 'full_name': self.project
740 }
741 },
742 'head': {
743 'sha': self.head_sha
744 }
745 },
746 'review': {
747 'state': review
748 },
749 'repository': {
750 'full_name': self.project
751 },
752 'sender': {
753 'login': 'ghuser'
754 }
755 }
756 return (name, data)
757
Jan Hruban16ad31f2015-11-07 14:39:07 +0100758 def addLabel(self, name):
759 if name not in self.labels:
760 self.labels.append(name)
761 self._updateTimeStamp()
762 return self._getLabelEvent(name)
763
764 def removeLabel(self, name):
765 if name in self.labels:
766 self.labels.remove(name)
767 self._updateTimeStamp()
768 return self._getUnlabelEvent(name)
769
770 def _getLabelEvent(self, label):
771 name = 'pull_request'
772 data = {
773 'action': 'labeled',
774 'pull_request': {
775 'number': self.number,
776 'updated_at': self.updated_at,
777 'base': {
778 'ref': self.branch,
779 'repo': {
780 'full_name': self.project
781 }
782 },
783 'head': {
784 'sha': self.head_sha
785 }
786 },
787 'label': {
788 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100789 },
790 'sender': {
791 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100792 }
793 }
794 return (name, data)
795
796 def _getUnlabelEvent(self, label):
797 name = 'pull_request'
798 data = {
799 'action': 'unlabeled',
800 'pull_request': {
801 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100802 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100803 'updated_at': self.updated_at,
804 'base': {
805 'ref': self.branch,
806 'repo': {
807 'full_name': self.project
808 }
809 },
810 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800811 'sha': self.head_sha,
812 'repo': {
813 'full_name': self.project
814 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100815 }
816 },
817 'label': {
818 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100819 },
820 'sender': {
821 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100822 }
823 }
824 return (name, data)
825
Jesse Keatinga41566f2017-06-14 18:17:51 -0700826 def editBody(self, body):
827 self.body = body
828 self._updateTimeStamp()
829
Gregory Haynes4fc12542015-04-22 20:38:06 -0700830 def _getRepo(self):
831 repo_path = os.path.join(self.upstream_root, self.project)
832 return git.Repo(repo_path)
833
834 def _createPRRef(self):
835 repo = self._getRepo()
836 GithubChangeReference.create(
837 repo, self._getPRReference(), 'refs/tags/init')
838
Jan Hruban570d01c2016-03-10 21:51:32 +0100839 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700840 repo = self._getRepo()
841 ref = repo.references[self._getPRReference()]
842 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100843 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700844 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100845 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700846 repo.head.reference = ref
847 zuul.merger.merger.reset_repo_to_head(repo)
848 repo.git.clean('-x', '-f', '-d')
849
Jan Hruban570d01c2016-03-10 21:51:32 +0100850 if files:
851 fn = files[0]
852 self.files = files
853 else:
854 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
855 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100856 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700857 fn = os.path.join(repo.working_dir, fn)
858 f = open(fn, 'w')
859 with open(fn, 'w') as f:
860 f.write("test %s %s\n" %
861 (self.branch, self.number))
862 repo.index.add([fn])
863
864 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800865 # Create an empty set of statuses for the given sha,
866 # each sha on a PR may have a status set on it
867 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700868 repo.head.reference = 'master'
869 zuul.merger.merger.reset_repo_to_head(repo)
870 repo.git.clean('-x', '-f', '-d')
871 repo.heads['master'].checkout()
872
873 def _updateTimeStamp(self):
874 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
875
876 def getPRHeadSha(self):
877 repo = self._getRepo()
878 return repo.references[self._getPRReference()].commit.hexsha
879
Jesse Keatingae4cd272017-01-30 17:10:44 -0800880 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800881 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
882 # convert the timestamp to a str format that would be returned
883 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800884
Adam Gandelmand81dd762017-02-09 15:15:49 -0800885 if granted_on:
886 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
887 submitted_at = time.strftime(
888 gh_time_format, granted_on.timetuple())
889 else:
890 # github timestamps only down to the second, so we need to make
891 # sure reviews that tests add appear to be added over a period of
892 # time in the past and not all at once.
893 if not self.reviews:
894 # the first review happens 10 mins ago
895 offset = 600
896 else:
897 # subsequent reviews happen 1 minute closer to now
898 offset = 600 - (len(self.reviews) * 60)
899
900 granted_on = datetime.datetime.utcfromtimestamp(
901 time.time() - offset)
902 submitted_at = time.strftime(
903 gh_time_format, granted_on.timetuple())
904
Jesse Keatingae4cd272017-01-30 17:10:44 -0800905 self.reviews.append({
906 'state': state,
907 'user': {
908 'login': user,
909 'email': user + "@derp.com",
910 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800911 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800912 })
913
Gregory Haynes4fc12542015-04-22 20:38:06 -0700914 def _getPRReference(self):
915 return '%s/head' % self.number
916
917 def _getPullRequestEvent(self, action):
918 name = 'pull_request'
919 data = {
920 'action': action,
921 'number': self.number,
922 'pull_request': {
923 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100924 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700925 'updated_at': self.updated_at,
926 'base': {
927 'ref': self.branch,
928 'repo': {
929 'full_name': self.project
930 }
931 },
932 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800933 'sha': self.head_sha,
934 'repo': {
935 'full_name': self.project
936 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700937 },
938 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100939 },
940 'sender': {
941 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700942 }
943 }
944 return (name, data)
945
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800946 def getCommitStatusEvent(self, context, state='success', user='zuul'):
947 name = 'status'
948 data = {
949 'state': state,
950 'sha': self.head_sha,
951 'description': 'Test results for %s: %s' % (self.head_sha, state),
952 'target_url': 'http://zuul/%s' % self.head_sha,
953 'branches': [],
954 'context': context,
955 'sender': {
956 'login': user
957 }
958 }
959 return (name, data)
960
James E. Blair289f5932017-07-27 15:02:29 -0700961 def setMerged(self, commit_message):
962 self.is_merged = True
963 self.merge_message = commit_message
964
965 repo = self._getRepo()
966 repo.heads[self.branch].commit = repo.commit(self.head_sha)
967
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968
969class FakeGithubConnection(githubconnection.GithubConnection):
970 log = logging.getLogger("zuul.test.FakeGithubConnection")
971
972 def __init__(self, driver, connection_name, connection_config,
973 upstream_root=None):
974 super(FakeGithubConnection, self).__init__(driver, connection_name,
975 connection_config)
976 self.connection_name = connection_name
977 self.pr_number = 0
978 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700979 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700980 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100981 self.merge_failure = False
982 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100983 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +0200984 self.github_client = FakeGithub()
985
986 def getGithubClient(self,
987 project=None,
988 user_id=None,
989 use_app=True):
990 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700991
Jesse Keatinga41566f2017-06-14 18:17:51 -0700992 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700993 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700994 self.pr_number += 1
995 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100996 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700997 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700998 self.pull_requests.append(pull_request)
999 return pull_request
1000
Jesse Keating71a47ff2017-06-06 11:36:43 -07001001 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1002 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001003 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001004 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001005 if not new_rev:
1006 new_rev = random_sha1()
1007 name = 'push'
1008 data = {
1009 'ref': ref,
1010 'before': old_rev,
1011 'after': new_rev,
1012 'repository': {
1013 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001014 },
1015 'commits': [
1016 {
1017 'added': added_files,
1018 'removed': removed_files,
1019 'modified': modified_files
1020 }
1021 ]
Wayne1a78c612015-06-11 17:14:13 -07001022 }
1023 return (name, data)
1024
Gregory Haynes4fc12542015-04-22 20:38:06 -07001025 def emitEvent(self, event):
1026 """Emulates sending the GitHub webhook event to the connection."""
1027 port = self.webapp.server.socket.getsockname()[1]
1028 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001029 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001030 secret = self.connection_config['webhook_token']
1031 signature = githubconnection._sign_request(payload, secret)
1032 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001033 req = urllib.request.Request(
1034 'http://localhost:%s/connection/%s/payload'
1035 % (port, self.connection_name),
1036 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001037 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001038
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001039 def addProject(self, project):
1040 # use the original method here and additionally register it in the
1041 # fake github
1042 super(FakeGithubConnection, self).addProject(project)
1043 self.getGithubClient(project).addProject(project)
1044
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001045 def getPull(self, project, number):
1046 pr = self.pull_requests[number - 1]
1047 data = {
1048 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001049 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001050 'updated_at': pr.updated_at,
1051 'base': {
1052 'repo': {
1053 'full_name': pr.project
1054 },
1055 'ref': pr.branch,
1056 },
Jan Hruban37615e52015-11-19 14:30:49 +01001057 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001058 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001059 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001060 'sha': pr.head_sha,
1061 'repo': {
1062 'full_name': pr.project
1063 }
Jesse Keating61040e72017-06-08 15:08:27 -07001064 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001065 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001066 'labels': pr.labels,
1067 'merged': pr.is_merged,
1068 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001069 }
1070 return data
1071
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001072 def getPullBySha(self, sha):
1073 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
1074 if len(prs) > 1:
1075 raise Exception('Multiple pulls found with head sha: %s' % sha)
1076 pr = prs[0]
1077 return self.getPull(pr.project, pr.number)
1078
Jesse Keatingae4cd272017-01-30 17:10:44 -08001079 def _getPullReviews(self, owner, project, number):
1080 pr = self.pull_requests[number - 1]
1081 return pr.reviews
1082
Jesse Keatingae4cd272017-01-30 17:10:44 -08001083 def getRepoPermission(self, project, login):
1084 owner, proj = project.split('/')
1085 for pr in self.pull_requests:
1086 pr_owner, pr_project = pr.project.split('/')
1087 if (pr_owner == owner and proj == pr_project):
1088 if login in pr.writers:
1089 return 'write'
1090 else:
1091 return 'read'
1092
Gregory Haynes4fc12542015-04-22 20:38:06 -07001093 def getGitUrl(self, project):
1094 return os.path.join(self.upstream_root, str(project))
1095
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001096 def real_getGitUrl(self, project):
1097 return super(FakeGithubConnection, self).getGitUrl(project)
1098
Jan Hrubane252a732017-01-03 15:03:09 +01001099 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001100 # record that this got reported
1101 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001102 pull_request = self.pull_requests[pr_number - 1]
1103 pull_request.addComment(message)
1104
Jan Hruban3b415922016-02-03 13:10:22 +01001105 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001106 # record that this got reported
1107 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001108 pull_request = self.pull_requests[pr_number - 1]
1109 if self.merge_failure:
1110 raise Exception('Pull request was not merged')
1111 if self.merge_not_allowed_count > 0:
1112 self.merge_not_allowed_count -= 1
1113 raise MergeFailure('Merge was not successful due to mergeability'
1114 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001115 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001116
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001117 def setCommitStatus(self, project, sha, state, url='', description='',
1118 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001119 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001120 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001121 super(FakeGithubConnection, self).setCommitStatus(
1122 project, sha, state,
1123 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001124
Jan Hruban16ad31f2015-11-07 14:39:07 +01001125 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001126 # record that this got reported
1127 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001128 pull_request = self.pull_requests[pr_number - 1]
1129 pull_request.addLabel(label)
1130
1131 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001132 # record that this got reported
1133 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001134 pull_request = self.pull_requests[pr_number - 1]
1135 pull_request.removeLabel(label)
1136
Jesse Keatinga41566f2017-06-14 18:17:51 -07001137 def _getNeededByFromPR(self, change):
1138 prs = []
1139 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001140 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001141 change.number))
1142 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001143 if not pr.body:
1144 body = ''
1145 else:
1146 body = pr.body
1147 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001148 # Get our version of a pull so that it's a dict
1149 pull = self.getPull(pr.project, pr.number)
1150 prs.append(pull)
1151
1152 return prs
1153
Gregory Haynes4fc12542015-04-22 20:38:06 -07001154
Clark Boylanb640e052014-04-03 16:41:46 -07001155class BuildHistory(object):
1156 def __init__(self, **kw):
1157 self.__dict__.update(kw)
1158
1159 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001160 return ("<Completed build, result: %s name: %s uuid: %s "
1161 "changes: %s ref: %s>" %
1162 (self.result, self.name, self.uuid,
1163 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001164
1165
Clark Boylanb640e052014-04-03 16:41:46 -07001166class FakeStatsd(threading.Thread):
1167 def __init__(self):
1168 threading.Thread.__init__(self)
1169 self.daemon = True
1170 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1171 self.sock.bind(('', 0))
1172 self.port = self.sock.getsockname()[1]
1173 self.wake_read, self.wake_write = os.pipe()
1174 self.stats = []
1175
1176 def run(self):
1177 while True:
1178 poll = select.poll()
1179 poll.register(self.sock, select.POLLIN)
1180 poll.register(self.wake_read, select.POLLIN)
1181 ret = poll.poll()
1182 for (fd, event) in ret:
1183 if fd == self.sock.fileno():
1184 data = self.sock.recvfrom(1024)
1185 if not data:
1186 return
1187 self.stats.append(data[0])
1188 if fd == self.wake_read:
1189 return
1190
1191 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001192 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001193
1194
James E. Blaire1767bc2016-08-02 10:00:27 -07001195class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001196 log = logging.getLogger("zuul.test")
1197
Paul Belanger174a8272017-03-14 13:20:10 -04001198 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001199 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001200 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001201 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001202 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001203 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001204 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001205 # TODOv3(jeblair): self.node is really "the label of the node
1206 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001207 # keep using it like this, or we may end up exposing more of
1208 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001209 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001210 self.node = None
1211 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001212 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001213 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001214 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001215 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001216 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001217 self.wait_condition = threading.Condition()
1218 self.waiting = False
1219 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001220 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001221 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001222 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001223 items = self.parameters['zuul']['items']
1224 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1225 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001226
James E. Blair3158e282016-08-19 09:34:11 -07001227 def __repr__(self):
1228 waiting = ''
1229 if self.waiting:
1230 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001231 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1232 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001233
Clark Boylanb640e052014-04-03 16:41:46 -07001234 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001235 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001236 self.wait_condition.acquire()
1237 self.wait_condition.notify()
1238 self.waiting = False
1239 self.log.debug("Build %s released" % self.unique)
1240 self.wait_condition.release()
1241
1242 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001243 """Return whether this build is being held.
1244
1245 :returns: Whether the build is being held.
1246 :rtype: bool
1247 """
1248
Clark Boylanb640e052014-04-03 16:41:46 -07001249 self.wait_condition.acquire()
1250 if self.waiting:
1251 ret = True
1252 else:
1253 ret = False
1254 self.wait_condition.release()
1255 return ret
1256
1257 def _wait(self):
1258 self.wait_condition.acquire()
1259 self.waiting = True
1260 self.log.debug("Build %s waiting" % self.unique)
1261 self.wait_condition.wait()
1262 self.wait_condition.release()
1263
1264 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001265 self.log.debug('Running build %s' % self.unique)
1266
Paul Belanger174a8272017-03-14 13:20:10 -04001267 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001268 self.log.debug('Holding build %s' % self.unique)
1269 self._wait()
1270 self.log.debug("Build %s continuing" % self.unique)
1271
James E. Blair412fba82017-01-26 15:00:50 -08001272 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001273 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001274 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001275 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001276 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001277 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001278 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001279
James E. Blaire1767bc2016-08-02 10:00:27 -07001280 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001281
James E. Blaira5dba232016-08-08 15:53:24 -07001282 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001283 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001284 for change in changes:
1285 if self.hasChanges(change):
1286 return True
1287 return False
1288
James E. Blaire7b99a02016-08-05 14:27:34 -07001289 def hasChanges(self, *changes):
1290 """Return whether this build has certain changes in its git repos.
1291
1292 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001293 are expected to be present (in order) in the git repository of
1294 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001295
1296 :returns: Whether the build has the indicated changes.
1297 :rtype: bool
1298
1299 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001300 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001301 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001302 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001303 try:
1304 repo = git.Repo(path)
1305 except NoSuchPathError as e:
1306 self.log.debug('%s' % e)
1307 return False
James E. Blair247cab72017-07-20 16:52:36 -07001308 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001309 commit_message = '%s-1' % change.subject
1310 self.log.debug("Checking if build %s has changes; commit_message "
1311 "%s; repo_messages %s" % (self, commit_message,
1312 repo_messages))
1313 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001314 self.log.debug(" messages do not match")
1315 return False
1316 self.log.debug(" OK")
1317 return True
1318
James E. Blaird8af5422017-05-24 13:59:40 -07001319 def getWorkspaceRepos(self, projects):
1320 """Return workspace git repo objects for the listed projects
1321
1322 :arg list projects: A list of strings, each the canonical name
1323 of a project.
1324
1325 :returns: A dictionary of {name: repo} for every listed
1326 project.
1327 :rtype: dict
1328
1329 """
1330
1331 repos = {}
1332 for project in projects:
1333 path = os.path.join(self.jobdir.src_root, project)
1334 repo = git.Repo(path)
1335 repos[project] = repo
1336 return repos
1337
Clark Boylanb640e052014-04-03 16:41:46 -07001338
Paul Belanger174a8272017-03-14 13:20:10 -04001339class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1340 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001341
Paul Belanger174a8272017-03-14 13:20:10 -04001342 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001343 they will report that they have started but then pause until
1344 released before reporting completion. This attribute may be
1345 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001346 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001347 be explicitly released.
1348
1349 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001350 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001351 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001352 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001353 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001354 self.hold_jobs_in_build = False
1355 self.lock = threading.Lock()
1356 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001357 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001358 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001359 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001360
James E. Blaira5dba232016-08-08 15:53:24 -07001361 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001362 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001363
1364 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001365 :arg Change change: The :py:class:`~tests.base.FakeChange`
1366 instance which should cause the job to fail. This job
1367 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001368
1369 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001370 l = self.fail_tests.get(name, [])
1371 l.append(change)
1372 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001373
James E. Blair962220f2016-08-03 11:22:38 -07001374 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001375 """Release a held build.
1376
1377 :arg str regex: A regular expression which, if supplied, will
1378 cause only builds with matching names to be released. If
1379 not supplied, all builds will be released.
1380
1381 """
James E. Blair962220f2016-08-03 11:22:38 -07001382 builds = self.running_builds[:]
1383 self.log.debug("Releasing build %s (%s)" % (regex,
1384 len(self.running_builds)))
1385 for build in builds:
1386 if not regex or re.match(regex, build.name):
1387 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001388 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001389 build.release()
1390 else:
1391 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001392 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001393 self.log.debug("Done releasing builds %s (%s)" %
1394 (regex, len(self.running_builds)))
1395
Paul Belanger174a8272017-03-14 13:20:10 -04001396 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001397 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001398 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001399 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001400 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001401 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001402 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001403 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001404 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1405 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001406
1407 def stopJob(self, job):
1408 self.log.debug("handle stop")
1409 parameters = json.loads(job.arguments)
1410 uuid = parameters['uuid']
1411 for build in self.running_builds:
1412 if build.unique == uuid:
1413 build.aborted = True
1414 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001415 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001416
James E. Blaira002b032017-04-18 10:35:48 -07001417 def stop(self):
1418 for build in self.running_builds:
1419 build.release()
1420 super(RecordingExecutorServer, self).stop()
1421
Joshua Hesketh50c21782016-10-13 21:34:14 +11001422
Paul Belanger174a8272017-03-14 13:20:10 -04001423class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001424 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001425 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001426 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001427 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001428 if not commit: # merge conflict
1429 self.recordResult('MERGER_FAILURE')
1430 return commit
1431
1432 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001433 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001434 self.executor_server.lock.acquire()
1435 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001436 BuildHistory(name=build.name, result=result, changes=build.changes,
1437 node=build.node, uuid=build.unique,
James E. Blair21037782017-07-19 11:56:55 -07001438 ref=build.parameters['zuul']['ref'],
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001439 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire675d682017-07-21 15:29:35 -07001440 pipeline=build.parameters['zuul']['pipeline'])
James E. Blaire1767bc2016-08-02 10:00:27 -07001441 )
Paul Belanger174a8272017-03-14 13:20:10 -04001442 self.executor_server.running_builds.remove(build)
1443 del self.executor_server.job_builds[self.job.unique]
1444 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001445
1446 def runPlaybooks(self, args):
1447 build = self.executor_server.job_builds[self.job.unique]
1448 build.jobdir = self.jobdir
1449
1450 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1451 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001452 return result
1453
James E. Blair74a82cf2017-07-12 17:23:08 -07001454 def runAnsible(self, cmd, timeout, config_file, trusted):
Paul Belanger174a8272017-03-14 13:20:10 -04001455 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001456
Paul Belanger174a8272017-03-14 13:20:10 -04001457 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001458 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blair74a82cf2017-07-12 17:23:08 -07001459 cmd, timeout, config_file, trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001460 else:
1461 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001462 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001463
James E. Blairad8dca02017-02-21 11:48:32 -05001464 def getHostList(self, args):
1465 self.log.debug("hostlist")
1466 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001467 for host in hosts:
1468 host['host_vars']['ansible_connection'] = 'local'
1469
1470 hosts.append(dict(
1471 name='localhost',
1472 host_vars=dict(ansible_connection='local'),
1473 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001474 return hosts
1475
James E. Blairf5dbd002015-12-23 15:26:17 -08001476
Clark Boylanb640e052014-04-03 16:41:46 -07001477class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001478 """A Gearman server for use in tests.
1479
1480 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1481 added to the queue but will not be distributed to workers
1482 until released. This attribute may be changed at any time and
1483 will take effect for subsequently enqueued jobs, but
1484 previously held jobs will still need to be explicitly
1485 released.
1486
1487 """
1488
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001489 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001490 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001491 if use_ssl:
1492 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1493 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1494 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1495 else:
1496 ssl_ca = None
1497 ssl_cert = None
1498 ssl_key = None
1499
1500 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1501 ssl_cert=ssl_cert,
1502 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001503
1504 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001505 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1506 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001507 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001508 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001509 job.waiting = self.hold_jobs_in_queue
1510 else:
1511 job.waiting = False
1512 if job.waiting:
1513 continue
1514 if job.name in connection.functions:
1515 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001516 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001517 connection.related_jobs[job.handle] = job
1518 job.worker_connection = connection
1519 job.running = True
1520 return job
1521 return None
1522
1523 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001524 """Release a held job.
1525
1526 :arg str regex: A regular expression which, if supplied, will
1527 cause only jobs with matching names to be released. If
1528 not supplied, all jobs will be released.
1529 """
Clark Boylanb640e052014-04-03 16:41:46 -07001530 released = False
1531 qlen = (len(self.high_queue) + len(self.normal_queue) +
1532 len(self.low_queue))
1533 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1534 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001535 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001536 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001537 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001538 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001539 self.log.debug("releasing queued job %s" %
1540 job.unique)
1541 job.waiting = False
1542 released = True
1543 else:
1544 self.log.debug("not releasing queued job %s" %
1545 job.unique)
1546 if released:
1547 self.wakeConnections()
1548 qlen = (len(self.high_queue) + len(self.normal_queue) +
1549 len(self.low_queue))
1550 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1551
1552
1553class FakeSMTP(object):
1554 log = logging.getLogger('zuul.FakeSMTP')
1555
1556 def __init__(self, messages, server, port):
1557 self.server = server
1558 self.port = port
1559 self.messages = messages
1560
1561 def sendmail(self, from_email, to_email, msg):
1562 self.log.info("Sending email from %s, to %s, with msg %s" % (
1563 from_email, to_email, msg))
1564
1565 headers = msg.split('\n\n', 1)[0]
1566 body = msg.split('\n\n', 1)[1]
1567
1568 self.messages.append(dict(
1569 from_email=from_email,
1570 to_email=to_email,
1571 msg=msg,
1572 headers=headers,
1573 body=body,
1574 ))
1575
1576 return True
1577
1578 def quit(self):
1579 return True
1580
1581
James E. Blairdce6cea2016-12-20 16:45:32 -08001582class FakeNodepool(object):
1583 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001584 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001585
1586 log = logging.getLogger("zuul.test.FakeNodepool")
1587
1588 def __init__(self, host, port, chroot):
1589 self.client = kazoo.client.KazooClient(
1590 hosts='%s:%s%s' % (host, port, chroot))
1591 self.client.start()
1592 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001593 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001594 self.thread = threading.Thread(target=self.run)
1595 self.thread.daemon = True
1596 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001597 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001598
1599 def stop(self):
1600 self._running = False
1601 self.thread.join()
1602 self.client.stop()
1603 self.client.close()
1604
1605 def run(self):
1606 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001607 try:
1608 self._run()
1609 except Exception:
1610 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001611 time.sleep(0.1)
1612
1613 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001614 if self.paused:
1615 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001616 for req in self.getNodeRequests():
1617 self.fulfillRequest(req)
1618
1619 def getNodeRequests(self):
1620 try:
1621 reqids = self.client.get_children(self.REQUEST_ROOT)
1622 except kazoo.exceptions.NoNodeError:
1623 return []
1624 reqs = []
1625 for oid in sorted(reqids):
1626 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001627 try:
1628 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001629 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001630 data['_oid'] = oid
1631 reqs.append(data)
1632 except kazoo.exceptions.NoNodeError:
1633 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001634 return reqs
1635
James E. Blaire18d4602017-01-05 11:17:28 -08001636 def getNodes(self):
1637 try:
1638 nodeids = self.client.get_children(self.NODE_ROOT)
1639 except kazoo.exceptions.NoNodeError:
1640 return []
1641 nodes = []
1642 for oid in sorted(nodeids):
1643 path = self.NODE_ROOT + '/' + oid
1644 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001645 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001646 data['_oid'] = oid
1647 try:
1648 lockfiles = self.client.get_children(path + '/lock')
1649 except kazoo.exceptions.NoNodeError:
1650 lockfiles = []
1651 if lockfiles:
1652 data['_lock'] = True
1653 else:
1654 data['_lock'] = False
1655 nodes.append(data)
1656 return nodes
1657
James E. Blaira38c28e2017-01-04 10:33:20 -08001658 def makeNode(self, request_id, node_type):
1659 now = time.time()
1660 path = '/nodepool/nodes/'
1661 data = dict(type=node_type,
1662 provider='test-provider',
1663 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001664 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001665 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001666 public_ipv4='127.0.0.1',
1667 private_ipv4=None,
1668 public_ipv6=None,
1669 allocated_to=request_id,
1670 state='ready',
1671 state_time=now,
1672 created_time=now,
1673 updated_time=now,
1674 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001675 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001676 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001677 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001678 path = self.client.create(path, data,
1679 makepath=True,
1680 sequence=True)
1681 nodeid = path.split("/")[-1]
1682 return nodeid
1683
James E. Blair6ab79e02017-01-06 10:10:17 -08001684 def addFailRequest(self, request):
1685 self.fail_requests.add(request['_oid'])
1686
James E. Blairdce6cea2016-12-20 16:45:32 -08001687 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001688 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001689 return
1690 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001691 oid = request['_oid']
1692 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001693
James E. Blair6ab79e02017-01-06 10:10:17 -08001694 if oid in self.fail_requests:
1695 request['state'] = 'failed'
1696 else:
1697 request['state'] = 'fulfilled'
1698 nodes = []
1699 for node in request['node_types']:
1700 nodeid = self.makeNode(oid, node)
1701 nodes.append(nodeid)
1702 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001703
James E. Blaira38c28e2017-01-04 10:33:20 -08001704 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001705 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001706 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001707 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001708 try:
1709 self.client.set(path, data)
1710 except kazoo.exceptions.NoNodeError:
1711 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001712
1713
James E. Blair498059b2016-12-20 13:50:13 -08001714class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001715 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001716 super(ChrootedKazooFixture, self).__init__()
1717
1718 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1719 if ':' in zk_host:
1720 host, port = zk_host.split(':')
1721 else:
1722 host = zk_host
1723 port = None
1724
1725 self.zookeeper_host = host
1726
1727 if not port:
1728 self.zookeeper_port = 2181
1729 else:
1730 self.zookeeper_port = int(port)
1731
Clark Boylan621ec9a2017-04-07 17:41:33 -07001732 self.test_id = test_id
1733
James E. Blair498059b2016-12-20 13:50:13 -08001734 def _setUp(self):
1735 # Make sure the test chroot paths do not conflict
1736 random_bits = ''.join(random.choice(string.ascii_lowercase +
1737 string.ascii_uppercase)
1738 for x in range(8))
1739
Clark Boylan621ec9a2017-04-07 17:41:33 -07001740 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001741 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1742
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001743 self.addCleanup(self._cleanup)
1744
James E. Blair498059b2016-12-20 13:50:13 -08001745 # Ensure the chroot path exists and clean up any pre-existing znodes.
1746 _tmp_client = kazoo.client.KazooClient(
1747 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1748 _tmp_client.start()
1749
1750 if _tmp_client.exists(self.zookeeper_chroot):
1751 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1752
1753 _tmp_client.ensure_path(self.zookeeper_chroot)
1754 _tmp_client.stop()
1755 _tmp_client.close()
1756
James E. Blair498059b2016-12-20 13:50:13 -08001757 def _cleanup(self):
1758 '''Remove the chroot path.'''
1759 # Need a non-chroot'ed client to remove the chroot path
1760 _tmp_client = kazoo.client.KazooClient(
1761 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1762 _tmp_client.start()
1763 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1764 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001765 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001766
1767
Joshua Heskethd78b4482015-09-14 16:56:34 -06001768class MySQLSchemaFixture(fixtures.Fixture):
1769 def setUp(self):
1770 super(MySQLSchemaFixture, self).setUp()
1771
1772 random_bits = ''.join(random.choice(string.ascii_lowercase +
1773 string.ascii_uppercase)
1774 for x in range(8))
1775 self.name = '%s_%s' % (random_bits, os.getpid())
1776 self.passwd = uuid.uuid4().hex
1777 db = pymysql.connect(host="localhost",
1778 user="openstack_citest",
1779 passwd="openstack_citest",
1780 db="openstack_citest")
1781 cur = db.cursor()
1782 cur.execute("create database %s" % self.name)
1783 cur.execute(
1784 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1785 (self.name, self.name, self.passwd))
1786 cur.execute("flush privileges")
1787
1788 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1789 self.passwd,
1790 self.name)
1791 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1792 self.addCleanup(self.cleanup)
1793
1794 def cleanup(self):
1795 db = pymysql.connect(host="localhost",
1796 user="openstack_citest",
1797 passwd="openstack_citest",
1798 db="openstack_citest")
1799 cur = db.cursor()
1800 cur.execute("drop database %s" % self.name)
1801 cur.execute("drop user '%s'@'localhost'" % self.name)
1802 cur.execute("flush privileges")
1803
1804
Maru Newby3fe5f852015-01-13 04:22:14 +00001805class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001806 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001807 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001808
James E. Blair1c236df2017-02-01 14:07:24 -08001809 def attachLogs(self, *args):
1810 def reader():
1811 self._log_stream.seek(0)
1812 while True:
1813 x = self._log_stream.read(4096)
1814 if not x:
1815 break
1816 yield x.encode('utf8')
1817 content = testtools.content.content_from_reader(
1818 reader,
1819 testtools.content_type.UTF8_TEXT,
1820 False)
1821 self.addDetail('logging', content)
1822
Clark Boylanb640e052014-04-03 16:41:46 -07001823 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001824 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001825 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1826 try:
1827 test_timeout = int(test_timeout)
1828 except ValueError:
1829 # If timeout value is invalid do not set a timeout.
1830 test_timeout = 0
1831 if test_timeout > 0:
1832 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1833
1834 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1835 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1836 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1837 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1838 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1839 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1840 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1841 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1842 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1843 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001844 self._log_stream = StringIO()
1845 self.addOnException(self.attachLogs)
1846 else:
1847 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001848
James E. Blair73b41772017-05-22 13:22:55 -07001849 # NOTE(jeblair): this is temporary extra debugging to try to
1850 # track down a possible leak.
1851 orig_git_repo_init = git.Repo.__init__
1852
1853 def git_repo_init(myself, *args, **kw):
1854 orig_git_repo_init(myself, *args, **kw)
1855 self.log.debug("Created git repo 0x%x %s" %
1856 (id(myself), repr(myself)))
1857
1858 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1859 git_repo_init))
1860
James E. Blair1c236df2017-02-01 14:07:24 -08001861 handler = logging.StreamHandler(self._log_stream)
1862 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1863 '%(levelname)-8s %(message)s')
1864 handler.setFormatter(formatter)
1865
1866 logger = logging.getLogger()
1867 logger.setLevel(logging.DEBUG)
1868 logger.addHandler(handler)
1869
Clark Boylan3410d532017-04-25 12:35:29 -07001870 # Make sure we don't carry old handlers around in process state
1871 # which slows down test runs
1872 self.addCleanup(logger.removeHandler, handler)
1873 self.addCleanup(handler.close)
1874 self.addCleanup(handler.flush)
1875
James E. Blair1c236df2017-02-01 14:07:24 -08001876 # NOTE(notmorgan): Extract logging overrides for specific
1877 # libraries from the OS_LOG_DEFAULTS env and create loggers
1878 # for each. This is used to limit the output during test runs
1879 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001880 log_defaults_from_env = os.environ.get(
1881 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001882 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001883
James E. Blairdce6cea2016-12-20 16:45:32 -08001884 if log_defaults_from_env:
1885 for default in log_defaults_from_env.split(','):
1886 try:
1887 name, level_str = default.split('=', 1)
1888 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001889 logger = logging.getLogger(name)
1890 logger.setLevel(level)
1891 logger.addHandler(handler)
1892 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001893 except ValueError:
1894 # NOTE(notmorgan): Invalid format of the log default,
1895 # skip and don't try and apply a logger for the
1896 # specified module
1897 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001898
Maru Newby3fe5f852015-01-13 04:22:14 +00001899
1900class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001901 """A test case with a functioning Zuul.
1902
1903 The following class variables are used during test setup and can
1904 be overidden by subclasses but are effectively read-only once a
1905 test method starts running:
1906
1907 :cvar str config_file: This points to the main zuul config file
1908 within the fixtures directory. Subclasses may override this
1909 to obtain a different behavior.
1910
1911 :cvar str tenant_config_file: This is the tenant config file
1912 (which specifies from what git repos the configuration should
1913 be loaded). It defaults to the value specified in
1914 `config_file` but can be overidden by subclasses to obtain a
1915 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001916 configuration. See also the :py:func:`simple_layout`
1917 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001918
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001919 :cvar bool create_project_keys: Indicates whether Zuul should
1920 auto-generate keys for each project, or whether the test
1921 infrastructure should insert dummy keys to save time during
1922 startup. Defaults to False.
1923
James E. Blaire7b99a02016-08-05 14:27:34 -07001924 The following are instance variables that are useful within test
1925 methods:
1926
1927 :ivar FakeGerritConnection fake_<connection>:
1928 A :py:class:`~tests.base.FakeGerritConnection` will be
1929 instantiated for each connection present in the config file
1930 and stored here. For instance, `fake_gerrit` will hold the
1931 FakeGerritConnection object for a connection named `gerrit`.
1932
1933 :ivar FakeGearmanServer gearman_server: An instance of
1934 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1935 server that all of the Zuul components in this test use to
1936 communicate with each other.
1937
Paul Belanger174a8272017-03-14 13:20:10 -04001938 :ivar RecordingExecutorServer executor_server: An instance of
1939 :py:class:`~tests.base.RecordingExecutorServer` which is the
1940 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001941
1942 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1943 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001944 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001945 list upon completion.
1946
1947 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1948 objects representing completed builds. They are appended to
1949 the list in the order they complete.
1950
1951 """
1952
James E. Blair83005782015-12-11 14:46:03 -08001953 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001954 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001955 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001956 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001957
1958 def _startMerger(self):
1959 self.merge_server = zuul.merger.server.MergeServer(self.config,
1960 self.connections)
1961 self.merge_server.start()
1962
Maru Newby3fe5f852015-01-13 04:22:14 +00001963 def setUp(self):
1964 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001965
1966 self.setupZK()
1967
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001968 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001969 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001970 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1971 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001972 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001973 tmp_root = tempfile.mkdtemp(
1974 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001975 self.test_root = os.path.join(tmp_root, "zuul-test")
1976 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001977 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001978 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001979 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001980 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1981 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001982
1983 if os.path.exists(self.test_root):
1984 shutil.rmtree(self.test_root)
1985 os.makedirs(self.test_root)
1986 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001987 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001988 os.makedirs(self.merger_state_root)
1989 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001990
1991 # Make per test copy of Configuration.
1992 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001993 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1994 if not os.path.exists(self.private_key_file):
1995 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1996 shutil.copy(src_private_key_file, self.private_key_file)
1997 shutil.copy('{}.pub'.format(src_private_key_file),
1998 '{}.pub'.format(self.private_key_file))
1999 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002000 self.config.set('scheduler', 'tenant_config',
2001 os.path.join(
2002 FIXTURE_DIR,
2003 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002004 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002005 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002006 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002007 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002008 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002009
Clark Boylanb640e052014-04-03 16:41:46 -07002010 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10002011 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
2012 # see: https://github.com/jsocol/pystatsd/issues/61
2013 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07002014 os.environ['STATSD_PORT'] = str(self.statsd.port)
2015 self.statsd.start()
2016 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05002017 importlib.reload(statsd)
2018 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07002019
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002020 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002021
2022 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002023 self.log.info("Gearman server on port %s" %
2024 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002025 if self.use_ssl:
2026 self.log.info('SSL enabled for gearman')
2027 self.config.set(
2028 'gearman', 'ssl_ca',
2029 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2030 self.config.set(
2031 'gearman', 'ssl_cert',
2032 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2033 self.config.set(
2034 'gearman', 'ssl_key',
2035 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002036
James E. Blaire511d2f2016-12-08 15:22:26 -08002037 gerritsource.GerritSource.replication_timeout = 1.5
2038 gerritsource.GerritSource.replication_retry_interval = 0.5
2039 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002040
Joshua Hesketh352264b2015-08-11 23:42:08 +10002041 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07002042
Jan Hruban7083edd2015-08-21 14:00:54 +02002043 self.webapp = zuul.webapp.WebApp(
2044 self.sched, port=0, listen_address='127.0.0.1')
2045
Jan Hruban6b71aff2015-10-22 16:58:08 +02002046 self.event_queues = [
2047 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002048 self.sched.trigger_event_queue,
2049 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002050 ]
2051
James E. Blairfef78942016-03-11 16:28:56 -08002052 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002053 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002054
Paul Belanger174a8272017-03-14 13:20:10 -04002055 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002056 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002057 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002058 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002059 _test_root=self.test_root,
2060 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002061 self.executor_server.start()
2062 self.history = self.executor_server.build_history
2063 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002064
Paul Belanger174a8272017-03-14 13:20:10 -04002065 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002066 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002067 self.merge_client = zuul.merger.client.MergeClient(
2068 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002069 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002070 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002071 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002072
James E. Blair0d5a36e2017-02-21 10:53:44 -05002073 self.fake_nodepool = FakeNodepool(
2074 self.zk_chroot_fixture.zookeeper_host,
2075 self.zk_chroot_fixture.zookeeper_port,
2076 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002077
Paul Belanger174a8272017-03-14 13:20:10 -04002078 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002079 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002080 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002081 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002082
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002083 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07002084
2085 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002086 self.webapp.start()
2087 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002088 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002089 # Cleanups are run in reverse order
2090 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002091 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002092 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002093
James E. Blairb9c0d772017-03-03 14:34:49 -08002094 self.sched.reconfigure(self.config)
2095 self.sched.resume()
2096
Tobias Henkel7df274b2017-05-26 17:41:11 +02002097 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002098 # Set up gerrit related fakes
2099 # Set a changes database so multiple FakeGerrit's can report back to
2100 # a virtual canonical database given by the configured hostname
2101 self.gerrit_changes_dbs = {}
2102
2103 def getGerritConnection(driver, name, config):
2104 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2105 con = FakeGerritConnection(driver, name, config,
2106 changes_db=db,
2107 upstream_root=self.upstream_root)
2108 self.event_queues.append(con.event_queue)
2109 setattr(self, 'fake_' + name, con)
2110 return con
2111
2112 self.useFixture(fixtures.MonkeyPatch(
2113 'zuul.driver.gerrit.GerritDriver.getConnection',
2114 getGerritConnection))
2115
Gregory Haynes4fc12542015-04-22 20:38:06 -07002116 def getGithubConnection(driver, name, config):
2117 con = FakeGithubConnection(driver, name, config,
2118 upstream_root=self.upstream_root)
2119 setattr(self, 'fake_' + name, con)
2120 return con
2121
2122 self.useFixture(fixtures.MonkeyPatch(
2123 'zuul.driver.github.GithubDriver.getConnection',
2124 getGithubConnection))
2125
James E. Blaire511d2f2016-12-08 15:22:26 -08002126 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002127 # TODO(jhesketh): This should come from lib.connections for better
2128 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002129 # Register connections from the config
2130 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002131
Joshua Hesketh352264b2015-08-11 23:42:08 +10002132 def FakeSMTPFactory(*args, **kw):
2133 args = [self.smtp_messages] + list(args)
2134 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002135
Joshua Hesketh352264b2015-08-11 23:42:08 +10002136 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002137
James E. Blaire511d2f2016-12-08 15:22:26 -08002138 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002139 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002140 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002141
James E. Blair83005782015-12-11 14:46:03 -08002142 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002143 # This creates the per-test configuration object. It can be
2144 # overriden by subclasses, but should not need to be since it
2145 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002146 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002147 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002148
James E. Blair39840362017-06-23 20:34:02 +01002149 sections = ['zuul', 'scheduler', 'executor', 'merger']
2150 for section in sections:
2151 if not self.config.has_section(section):
2152 self.config.add_section(section)
2153
James E. Blair06cc3922017-04-19 10:08:10 -07002154 if not self.setupSimpleLayout():
2155 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002156 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002157 self.tenant_config_file)
2158 git_path = os.path.join(
2159 os.path.dirname(
2160 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2161 'git')
2162 if os.path.exists(git_path):
2163 for reponame in os.listdir(git_path):
2164 project = reponame.replace('_', '/')
2165 self.copyDirToRepo(project,
2166 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002167 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002168 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002169 self.setupAllProjectKeys()
2170
James E. Blair06cc3922017-04-19 10:08:10 -07002171 def setupSimpleLayout(self):
2172 # If the test method has been decorated with a simple_layout,
2173 # use that instead of the class tenant_config_file. Set up a
2174 # single config-project with the specified layout, and
2175 # initialize repos for all of the 'project' entries which
2176 # appear in the layout.
2177 test_name = self.id().split('.')[-1]
2178 test = getattr(self, test_name)
2179 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002180 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002181 else:
2182 return False
2183
James E. Blairb70e55a2017-04-19 12:57:02 -07002184 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002185 path = os.path.join(FIXTURE_DIR, path)
2186 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002187 data = f.read()
2188 layout = yaml.safe_load(data)
2189 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002190 untrusted_projects = []
2191 for item in layout:
2192 if 'project' in item:
2193 name = item['project']['name']
2194 untrusted_projects.append(name)
2195 self.init_repo(name)
2196 self.addCommitToRepo(name, 'initial commit',
2197 files={'README': ''},
2198 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002199 if 'job' in item:
2200 jobname = item['job']['name']
2201 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002202
2203 root = os.path.join(self.test_root, "config")
2204 if not os.path.exists(root):
2205 os.makedirs(root)
2206 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2207 config = [{'tenant':
2208 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002209 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002210 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002211 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002212 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002213 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002214 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002215 os.path.join(FIXTURE_DIR, f.name))
2216
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002217 self.init_repo('org/common-config')
2218 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002219 files, branch='master', tag='init')
2220
2221 return True
2222
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002223 def setupAllProjectKeys(self):
2224 if self.create_project_keys:
2225 return
2226
James E. Blair39840362017-06-23 20:34:02 +01002227 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002228 with open(os.path.join(FIXTURE_DIR, path)) as f:
2229 tenant_config = yaml.safe_load(f.read())
2230 for tenant in tenant_config:
2231 sources = tenant['tenant']['source']
2232 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002233 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002234 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002235 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002236 self.setupProjectKeys(source, project)
2237
2238 def setupProjectKeys(self, source, project):
2239 # Make sure we set up an RSA key for the project so that we
2240 # don't spend time generating one:
2241
James E. Blair6459db12017-06-29 14:57:20 -07002242 if isinstance(project, dict):
2243 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002244 key_root = os.path.join(self.state_root, 'keys')
2245 if not os.path.isdir(key_root):
2246 os.mkdir(key_root, 0o700)
2247 private_key_file = os.path.join(key_root, source, project + '.pem')
2248 private_key_dir = os.path.dirname(private_key_file)
2249 self.log.debug("Installing test keys for project %s at %s" % (
2250 project, private_key_file))
2251 if not os.path.isdir(private_key_dir):
2252 os.makedirs(private_key_dir)
2253 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2254 with open(private_key_file, 'w') as o:
2255 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002256
James E. Blair498059b2016-12-20 13:50:13 -08002257 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002258 self.zk_chroot_fixture = self.useFixture(
2259 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002260 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002261 self.zk_chroot_fixture.zookeeper_host,
2262 self.zk_chroot_fixture.zookeeper_port,
2263 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002264
James E. Blair96c6bf82016-01-15 16:20:40 -08002265 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002266 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002267
2268 files = {}
2269 for (dirpath, dirnames, filenames) in os.walk(source_path):
2270 for filename in filenames:
2271 test_tree_filepath = os.path.join(dirpath, filename)
2272 common_path = os.path.commonprefix([test_tree_filepath,
2273 source_path])
2274 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2275 with open(test_tree_filepath, 'r') as f:
2276 content = f.read()
2277 files[relative_filepath] = content
2278 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002279 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002280
James E. Blaire18d4602017-01-05 11:17:28 -08002281 def assertNodepoolState(self):
2282 # Make sure that there are no pending requests
2283
2284 requests = self.fake_nodepool.getNodeRequests()
2285 self.assertEqual(len(requests), 0)
2286
2287 nodes = self.fake_nodepool.getNodes()
2288 for node in nodes:
2289 self.assertFalse(node['_lock'], "Node %s is locked" %
2290 (node['_oid'],))
2291
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002292 def assertNoGeneratedKeys(self):
2293 # Make sure that Zuul did not generate any project keys
2294 # (unless it was supposed to).
2295
2296 if self.create_project_keys:
2297 return
2298
2299 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2300 test_key = i.read()
2301
2302 key_root = os.path.join(self.state_root, 'keys')
2303 for root, dirname, files in os.walk(key_root):
2304 for fn in files:
2305 with open(os.path.join(root, fn)) as f:
2306 self.assertEqual(test_key, f.read())
2307
Clark Boylanb640e052014-04-03 16:41:46 -07002308 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002309 self.log.debug("Assert final state")
2310 # Make sure no jobs are running
2311 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002312 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002313 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002314 gc.collect()
2315 for obj in gc.get_objects():
2316 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002317 self.log.debug("Leaked git repo object: 0x%x %s" %
2318 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002319 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002320 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002321 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002322 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002323 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002324 for tenant in self.sched.abide.tenants.values():
2325 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002326 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002327 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002328
2329 def shutdown(self):
2330 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002331 self.executor_server.hold_jobs_in_build = False
2332 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002333 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002334 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002335 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002336 self.sched.stop()
2337 self.sched.join()
2338 self.statsd.stop()
2339 self.statsd.join()
2340 self.webapp.stop()
2341 self.webapp.join()
2342 self.rpc.stop()
2343 self.rpc.join()
2344 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002345 self.fake_nodepool.stop()
2346 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002347 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002348 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002349 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002350 # Further the pydevd threads also need to be whitelisted so debugging
2351 # e.g. in PyCharm is possible without breaking shutdown.
2352 whitelist = ['executor-watchdog',
2353 'pydevd.CommandThread',
2354 'pydevd.Reader',
2355 'pydevd.Writer',
2356 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002357 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002358 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002359 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002360 log_str = ""
2361 for thread_id, stack_frame in sys._current_frames().items():
2362 log_str += "Thread: %s\n" % thread_id
2363 log_str += "".join(traceback.format_stack(stack_frame))
2364 self.log.debug(log_str)
2365 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002366
James E. Blaira002b032017-04-18 10:35:48 -07002367 def assertCleanShutdown(self):
2368 pass
2369
James E. Blairc4ba97a2017-04-19 16:26:24 -07002370 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002371 parts = project.split('/')
2372 path = os.path.join(self.upstream_root, *parts[:-1])
2373 if not os.path.exists(path):
2374 os.makedirs(path)
2375 path = os.path.join(self.upstream_root, project)
2376 repo = git.Repo.init(path)
2377
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002378 with repo.config_writer() as config_writer:
2379 config_writer.set_value('user', 'email', 'user@example.com')
2380 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002381
Clark Boylanb640e052014-04-03 16:41:46 -07002382 repo.index.commit('initial commit')
2383 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002384 if tag:
2385 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002386
James E. Blair97d902e2014-08-21 13:25:56 -07002387 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002388 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002389 repo.git.clean('-x', '-f', '-d')
2390
James E. Blair97d902e2014-08-21 13:25:56 -07002391 def create_branch(self, project, branch):
2392 path = os.path.join(self.upstream_root, project)
2393 repo = git.Repo.init(path)
2394 fn = os.path.join(path, 'README')
2395
2396 branch_head = repo.create_head(branch)
2397 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002398 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002399 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002400 f.close()
2401 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002402 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002403
James E. Blair97d902e2014-08-21 13:25:56 -07002404 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002405 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002406 repo.git.clean('-x', '-f', '-d')
2407
Sachi King9f16d522016-03-16 12:20:45 +11002408 def create_commit(self, project):
2409 path = os.path.join(self.upstream_root, project)
2410 repo = git.Repo(path)
2411 repo.head.reference = repo.heads['master']
2412 file_name = os.path.join(path, 'README')
2413 with open(file_name, 'a') as f:
2414 f.write('creating fake commit\n')
2415 repo.index.add([file_name])
2416 commit = repo.index.commit('Creating a fake commit')
2417 return commit.hexsha
2418
James E. Blairf4a5f022017-04-18 14:01:10 -07002419 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002420 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002421 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002422 while len(self.builds):
2423 self.release(self.builds[0])
2424 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002425 i += 1
2426 if count is not None and i >= count:
2427 break
James E. Blairb8c16472015-05-05 14:55:26 -07002428
James E. Blairdf25ddc2017-07-08 07:57:09 -07002429 def getSortedBuilds(self):
2430 "Return the list of currently running builds sorted by name"
2431
2432 return sorted(self.builds, key=lambda x: x.name)
2433
Clark Boylanb640e052014-04-03 16:41:46 -07002434 def release(self, job):
2435 if isinstance(job, FakeBuild):
2436 job.release()
2437 else:
2438 job.waiting = False
2439 self.log.debug("Queued job %s released" % job.unique)
2440 self.gearman_server.wakeConnections()
2441
2442 def getParameter(self, job, name):
2443 if isinstance(job, FakeBuild):
2444 return job.parameters[name]
2445 else:
2446 parameters = json.loads(job.arguments)
2447 return parameters[name]
2448
Clark Boylanb640e052014-04-03 16:41:46 -07002449 def haveAllBuildsReported(self):
2450 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002451 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002452 return False
2453 # Find out if every build that the worker has completed has been
2454 # reported back to Zuul. If it hasn't then that means a Gearman
2455 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002456 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002457 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002458 if not zbuild:
2459 # It has already been reported
2460 continue
2461 # It hasn't been reported yet.
2462 return False
2463 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002464 worker = self.executor_server.executor_worker
2465 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002466 if connection.state == 'GRAB_WAIT':
2467 return False
2468 return True
2469
2470 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002471 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002472 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002473 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002474 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002475 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002476 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002477 for j in conn.related_jobs.values():
2478 if j.unique == build.uuid:
2479 client_job = j
2480 break
2481 if not client_job:
2482 self.log.debug("%s is not known to the gearman client" %
2483 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002484 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002485 if not client_job.handle:
2486 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002487 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002488 server_job = self.gearman_server.jobs.get(client_job.handle)
2489 if not server_job:
2490 self.log.debug("%s is not known to the gearman server" %
2491 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002492 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002493 if not hasattr(server_job, 'waiting'):
2494 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002495 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002496 if server_job.waiting:
2497 continue
James E. Blair17302972016-08-10 16:11:42 -07002498 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002499 self.log.debug("%s has not reported start" % build)
2500 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002501 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002502 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002503 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002504 if worker_build:
2505 if worker_build.isWaiting():
2506 continue
2507 else:
2508 self.log.debug("%s is running" % worker_build)
2509 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002510 else:
James E. Blair962220f2016-08-03 11:22:38 -07002511 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002512 return False
James E. Blaira002b032017-04-18 10:35:48 -07002513 for (build_uuid, job_worker) in \
2514 self.executor_server.job_workers.items():
2515 if build_uuid not in seen_builds:
2516 self.log.debug("%s is not finalized" % build_uuid)
2517 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002518 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002519
James E. Blairdce6cea2016-12-20 16:45:32 -08002520 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002521 if self.fake_nodepool.paused:
2522 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002523 if self.sched.nodepool.requests:
2524 return False
2525 return True
2526
Jan Hruban6b71aff2015-10-22 16:58:08 +02002527 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002528 for event_queue in self.event_queues:
2529 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002530
2531 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002532 for event_queue in self.event_queues:
2533 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002534
Clark Boylanb640e052014-04-03 16:41:46 -07002535 def waitUntilSettled(self):
2536 self.log.debug("Waiting until settled...")
2537 start = time.time()
2538 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002539 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002540 self.log.error("Timeout waiting for Zuul to settle")
2541 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002542 for event_queue in self.event_queues:
2543 self.log.error(" %s: %s" %
2544 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002545 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002546 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002547 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002548 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002549 self.log.error("All requests completed: %s" %
2550 (self.areAllNodeRequestsComplete(),))
2551 self.log.error("Merge client jobs: %s" %
2552 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002553 raise Exception("Timeout waiting for Zuul to settle")
2554 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002555
Paul Belanger174a8272017-03-14 13:20:10 -04002556 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002557 # have all build states propogated to zuul?
2558 if self.haveAllBuildsReported():
2559 # Join ensures that the queue is empty _and_ events have been
2560 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002561 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002562 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002563 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002564 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002565 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002566 self.areAllNodeRequestsComplete() and
2567 all(self.eventQueuesEmpty())):
2568 # The queue empty check is placed at the end to
2569 # ensure that if a component adds an event between
2570 # when locked the run handler and checked that the
2571 # components were stable, we don't erroneously
2572 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002573 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002574 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002575 self.log.debug("...settled.")
2576 return
2577 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002578 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002579 self.sched.wake_event.wait(0.1)
2580
2581 def countJobResults(self, jobs, result):
2582 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002583 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002584
Monty Taylor0d926122017-05-24 08:07:56 -05002585 def getBuildByName(self, name):
2586 for build in self.builds:
2587 if build.name == name:
2588 return build
2589 raise Exception("Unable to find build %s" % name)
2590
James E. Blair96c6bf82016-01-15 16:20:40 -08002591 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002592 for job in self.history:
2593 if (job.name == name and
2594 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002595 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002596 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002597 raise Exception("Unable to find job %s in history" % name)
2598
2599 def assertEmptyQueues(self):
2600 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002601 for tenant in self.sched.abide.tenants.values():
2602 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002603 for pipeline_queue in pipeline.queues:
2604 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002605 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002606 pipeline.name, pipeline_queue.name,
2607 pipeline_queue.queue))
2608 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002609 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002610
2611 def assertReportedStat(self, key, value=None, kind=None):
2612 start = time.time()
2613 while time.time() < (start + 5):
2614 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002615 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002616 if key == k:
2617 if value is None and kind is None:
2618 return
2619 elif value:
2620 if value == v:
2621 return
2622 elif kind:
2623 if v.endswith('|' + kind):
2624 return
2625 time.sleep(0.1)
2626
Clark Boylanb640e052014-04-03 16:41:46 -07002627 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002628
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002629 def assertBuilds(self, builds):
2630 """Assert that the running builds are as described.
2631
2632 The list of running builds is examined and must match exactly
2633 the list of builds described by the input.
2634
2635 :arg list builds: A list of dictionaries. Each item in the
2636 list must match the corresponding build in the build
2637 history, and each element of the dictionary must match the
2638 corresponding attribute of the build.
2639
2640 """
James E. Blair3158e282016-08-19 09:34:11 -07002641 try:
2642 self.assertEqual(len(self.builds), len(builds))
2643 for i, d in enumerate(builds):
2644 for k, v in d.items():
2645 self.assertEqual(
2646 getattr(self.builds[i], k), v,
2647 "Element %i in builds does not match" % (i,))
2648 except Exception:
2649 for build in self.builds:
2650 self.log.error("Running build: %s" % build)
2651 else:
2652 self.log.error("No running builds")
2653 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002654
James E. Blairb536ecc2016-08-31 10:11:42 -07002655 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002656 """Assert that the completed builds are as described.
2657
2658 The list of completed builds is examined and must match
2659 exactly the list of builds described by the input.
2660
2661 :arg list history: A list of dictionaries. Each item in the
2662 list must match the corresponding build in the build
2663 history, and each element of the dictionary must match the
2664 corresponding attribute of the build.
2665
James E. Blairb536ecc2016-08-31 10:11:42 -07002666 :arg bool ordered: If true, the history must match the order
2667 supplied, if false, the builds are permitted to have
2668 arrived in any order.
2669
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002670 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002671 def matches(history_item, item):
2672 for k, v in item.items():
2673 if getattr(history_item, k) != v:
2674 return False
2675 return True
James E. Blair3158e282016-08-19 09:34:11 -07002676 try:
2677 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002678 if ordered:
2679 for i, d in enumerate(history):
2680 if not matches(self.history[i], d):
2681 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002682 "Element %i in history does not match %s" %
2683 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002684 else:
2685 unseen = self.history[:]
2686 for i, d in enumerate(history):
2687 found = False
2688 for unseen_item in unseen:
2689 if matches(unseen_item, d):
2690 found = True
2691 unseen.remove(unseen_item)
2692 break
2693 if not found:
2694 raise Exception("No match found for element %i "
2695 "in history" % (i,))
2696 if unseen:
2697 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002698 except Exception:
2699 for build in self.history:
2700 self.log.error("Completed build: %s" % build)
2701 else:
2702 self.log.error("No completed builds")
2703 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002704
James E. Blair6ac368c2016-12-22 18:07:20 -08002705 def printHistory(self):
2706 """Log the build history.
2707
2708 This can be useful during tests to summarize what jobs have
2709 completed.
2710
2711 """
2712 self.log.debug("Build history:")
2713 for build in self.history:
2714 self.log.debug(build)
2715
James E. Blair59fdbac2015-12-07 17:08:06 -08002716 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002717 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2718
James E. Blair9ea70072017-04-19 16:05:30 -07002719 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002720 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002721 if not os.path.exists(root):
2722 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002723 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2724 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002725- tenant:
2726 name: openstack
2727 source:
2728 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002729 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002730 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002731 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002732 - org/project
2733 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002734 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002735 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002736 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002737 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002738 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002739
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002740 def addCommitToRepo(self, project, message, files,
2741 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002742 path = os.path.join(self.upstream_root, project)
2743 repo = git.Repo(path)
2744 repo.head.reference = branch
2745 zuul.merger.merger.reset_repo_to_head(repo)
2746 for fn, content in files.items():
2747 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002748 try:
2749 os.makedirs(os.path.dirname(fn))
2750 except OSError:
2751 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002752 with open(fn, 'w') as f:
2753 f.write(content)
2754 repo.index.add([fn])
2755 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002756 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002757 repo.heads[branch].commit = commit
2758 repo.head.reference = branch
2759 repo.git.clean('-x', '-f', '-d')
2760 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002761 if tag:
2762 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002763 return before
2764
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002765 def commitConfigUpdate(self, project_name, source_name):
2766 """Commit an update to zuul.yaml
2767
2768 This overwrites the zuul.yaml in the specificed project with
2769 the contents specified.
2770
2771 :arg str project_name: The name of the project containing
2772 zuul.yaml (e.g., common-config)
2773
2774 :arg str source_name: The path to the file (underneath the
2775 test fixture directory) whose contents should be used to
2776 replace zuul.yaml.
2777 """
2778
2779 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002780 files = {}
2781 with open(source_path, 'r') as f:
2782 data = f.read()
2783 layout = yaml.safe_load(data)
2784 files['zuul.yaml'] = data
2785 for item in layout:
2786 if 'job' in item:
2787 jobname = item['job']['name']
2788 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002789 before = self.addCommitToRepo(
2790 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002791 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002792 return before
2793
James E. Blair7fc8daa2016-08-08 15:37:15 -07002794 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002795
James E. Blair7fc8daa2016-08-08 15:37:15 -07002796 """Inject a Fake (Gerrit) event.
2797
2798 This method accepts a JSON-encoded event and simulates Zuul
2799 having received it from Gerrit. It could (and should)
2800 eventually apply to any connection type, but is currently only
2801 used with Gerrit connections. The name of the connection is
2802 used to look up the corresponding server, and the event is
2803 simulated as having been received by all Zuul connections
2804 attached to that server. So if two Gerrit connections in Zuul
2805 are connected to the same Gerrit server, and you invoke this
2806 method specifying the name of one of them, the event will be
2807 received by both.
2808
2809 .. note::
2810
2811 "self.fake_gerrit.addEvent" calls should be migrated to
2812 this method.
2813
2814 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002815 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002816 :arg str event: The JSON-encoded event.
2817
2818 """
2819 specified_conn = self.connections.connections[connection]
2820 for conn in self.connections.connections.values():
2821 if (isinstance(conn, specified_conn.__class__) and
2822 specified_conn.server == conn.server):
2823 conn.addEvent(event)
2824
James E. Blaird8af5422017-05-24 13:59:40 -07002825 def getUpstreamRepos(self, projects):
2826 """Return upstream git repo objects for the listed projects
2827
2828 :arg list projects: A list of strings, each the canonical name
2829 of a project.
2830
2831 :returns: A dictionary of {name: repo} for every listed
2832 project.
2833 :rtype: dict
2834
2835 """
2836
2837 repos = {}
2838 for project in projects:
2839 # FIXME(jeblair): the upstream root does not yet have a
2840 # hostname component; that needs to be added, and this
2841 # line removed:
2842 tmp_project_name = '/'.join(project.split('/')[1:])
2843 path = os.path.join(self.upstream_root, tmp_project_name)
2844 repo = git.Repo(path)
2845 repos[project] = repo
2846 return repos
2847
James E. Blair3f876d52016-07-22 13:07:14 -07002848
2849class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002850 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002851 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002852
Jamie Lennox7655b552017-03-17 12:33:38 +11002853 @contextmanager
2854 def jobLog(self, build):
2855 """Print job logs on assertion errors
2856
2857 This method is a context manager which, if it encounters an
2858 ecxeption, adds the build log to the debug output.
2859
2860 :arg Build build: The build that's being asserted.
2861 """
2862 try:
2863 yield
2864 except Exception:
2865 path = os.path.join(self.test_root, build.uuid,
2866 'work', 'logs', 'job-output.txt')
2867 with open(path) as f:
2868 self.log.debug(f.read())
2869 raise
2870
Joshua Heskethd78b4482015-09-14 16:56:34 -06002871
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002872class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002873 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002874 use_ssl = True
2875
2876
Joshua Heskethd78b4482015-09-14 16:56:34 -06002877class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002878 def setup_config(self):
2879 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002880 for section_name in self.config.sections():
2881 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2882 section_name, re.I)
2883 if not con_match:
2884 continue
2885
2886 if self.config.get(section_name, 'driver') == 'sql':
2887 f = MySQLSchemaFixture()
2888 self.useFixture(f)
2889 if (self.config.get(section_name, 'dburi') ==
2890 '$MYSQL_FIXTURE_DBURI$'):
2891 self.config.set(section_name, 'dburi', f.dburi)