blob: 0105ffaaadbd56a7142fea1f7bb4c3e41014380c [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
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
Clark Boylan8208c192017-04-24 18:08:08 -070041import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070042import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import uuid
44
Clark Boylanb640e052014-04-03 16:41:46 -070045
46import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import statsd
53import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080054import testtools.content
55import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080056from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000057import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070058
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
63import zuul.webapp
64import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040065import zuul.executor.server
66import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080067import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070068import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070069import zuul.merger.merger
70import zuul.merger.server
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):
James E. Blair8b5408c2016-08-08 15:37:46 -0700137 categories = {'approved': ('Approved', -1, 1),
138 'code-review': ('Code-Review', -2, 2),
139 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700140
141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
174 'url': 'https://hostname/%s' % number}
175
176 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
186 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
195 with open(fn, 'w') as f:
196 f.write(content)
197 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700198 else:
199 for fni in range(100):
200 fn = os.path.join(path, str(fni))
201 f = open(fn, 'w')
202 for ci in range(4096):
203 f.write(random.choice(string.printable))
204 f.close()
205 repo.index.add([fn])
206
207 r = repo.index.commit(msg)
208 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700209 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700210 repo.git.clean('-x', '-f', '-d')
211 repo.heads['master'].checkout()
212 return r
213
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700215 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700217 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 data = ("test %s %s %s\n" %
219 (self.branch, self.number, self.latest_patchset))
220 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700221 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700222 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700223 ps_files = [{'file': '/COMMIT_MSG',
224 'type': 'ADDED'},
225 {'file': 'README',
226 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700228 ps_files.append({'file': f, 'type': 'ADDED'})
229 d = {'approvals': [],
230 'createdOn': time.time(),
231 'files': ps_files,
232 'number': str(self.latest_patchset),
233 'ref': 'refs/changes/1/%s/%s' % (self.number,
234 self.latest_patchset),
235 'revision': c.hexsha,
236 'uploader': {'email': 'user@example.com',
237 'name': 'User name',
238 'username': 'user'}}
239 self.data['currentPatchSet'] = d
240 self.patchsets.append(d)
241 self.data['submitRecords'] = self.getSubmitRecords()
242
243 def getPatchsetCreatedEvent(self, patchset):
244 event = {"type": "patchset-created",
245 "change": {"project": self.project,
246 "branch": self.branch,
247 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
248 "number": str(self.number),
249 "subject": self.subject,
250 "owner": {"name": "User Name"},
251 "url": "https://hostname/3"},
252 "patchSet": self.patchsets[patchset - 1],
253 "uploader": {"name": "User Name"}}
254 return event
255
256 def getChangeRestoredEvent(self):
257 event = {"type": "change-restored",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100266 "patchSet": self.patchsets[-1],
267 "reason": ""}
268 return event
269
270 def getChangeAbandonedEvent(self):
271 event = {"type": "change-abandoned",
272 "change": {"project": self.project,
273 "branch": self.branch,
274 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
275 "number": str(self.number),
276 "subject": self.subject,
277 "owner": {"name": "User Name"},
278 "url": "https://hostname/3"},
279 "abandoner": {"name": "User Name"},
280 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700281 "reason": ""}
282 return event
283
284 def getChangeCommentEvent(self, patchset):
285 event = {"type": "comment-added",
286 "change": {"project": self.project,
287 "branch": self.branch,
288 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
289 "number": str(self.number),
290 "subject": self.subject,
291 "owner": {"name": "User Name"},
292 "url": "https://hostname/3"},
293 "patchSet": self.patchsets[patchset - 1],
294 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700295 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700296 "description": "Code-Review",
297 "value": "0"}],
298 "comment": "This is a comment"}
299 return event
300
James E. Blairc2a5ed72017-02-20 14:12:01 -0500301 def getChangeMergedEvent(self):
302 event = {"submitter": {"name": "Jenkins",
303 "username": "jenkins"},
304 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
305 "patchSet": self.patchsets[-1],
306 "change": self.data,
307 "type": "change-merged",
308 "eventCreatedOn": 1487613810}
309 return event
310
James E. Blair8cce42e2016-10-18 08:18:36 -0700311 def getRefUpdatedEvent(self):
312 path = os.path.join(self.upstream_root, self.project)
313 repo = git.Repo(path)
314 oldrev = repo.heads[self.branch].commit.hexsha
315
316 event = {
317 "type": "ref-updated",
318 "submitter": {
319 "name": "User Name",
320 },
321 "refUpdate": {
322 "oldRev": oldrev,
323 "newRev": self.patchsets[-1]['revision'],
324 "refName": self.branch,
325 "project": self.project,
326 }
327 }
328 return event
329
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 def addApproval(self, category, value, username='reviewer_john',
331 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700332 if not granted_on:
333 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000334 approval = {
335 'description': self.categories[category][0],
336 'type': category,
337 'value': str(value),
338 'by': {
339 'username': username,
340 'email': username + '@example.com',
341 },
342 'grantedOn': int(granted_on)
343 }
Clark Boylanb640e052014-04-03 16:41:46 -0700344 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
345 if x['by']['username'] == username and x['type'] == category:
346 del self.patchsets[-1]['approvals'][i]
347 self.patchsets[-1]['approvals'].append(approval)
348 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000349 'author': {'email': 'author@example.com',
350 'name': 'Patchset Author',
351 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700352 'change': {'branch': self.branch,
353 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
354 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'owner': {'email': 'owner@example.com',
356 'name': 'Change Owner',
357 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700358 'project': self.project,
359 'subject': self.subject,
360 'topic': 'master',
361 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'patchSet': self.patchsets[-1],
364 'type': 'comment-added'}
365 self.data['submitRecords'] = self.getSubmitRecords()
366 return json.loads(json.dumps(event))
367
368 def getSubmitRecords(self):
369 status = {}
370 for cat in self.categories.keys():
371 status[cat] = 0
372
373 for a in self.patchsets[-1]['approvals']:
374 cur = status[a['type']]
375 cat_min, cat_max = self.categories[a['type']][1:]
376 new = int(a['value'])
377 if new == cat_min:
378 cur = new
379 elif abs(new) > abs(cur):
380 cur = new
381 status[a['type']] = cur
382
383 labels = []
384 ok = True
385 for typ, cat in self.categories.items():
386 cur = status[typ]
387 cat_min, cat_max = cat[1:]
388 if cur == cat_min:
389 value = 'REJECT'
390 ok = False
391 elif cur == cat_max:
392 value = 'OK'
393 else:
394 value = 'NEED'
395 ok = False
396 labels.append({'label': cat[0], 'status': value})
397 if ok:
398 return [{'status': 'OK'}]
399 return [{'status': 'NOT_READY',
400 'labels': labels}]
401
402 def setDependsOn(self, other, patchset):
403 self.depends_on_change = other
404 d = {'id': other.data['id'],
405 'number': other.data['number'],
406 'ref': other.patchsets[patchset - 1]['ref']
407 }
408 self.data['dependsOn'] = [d]
409
410 other.needed_by_changes.append(self)
411 needed = other.data.get('neededBy', [])
412 d = {'id': self.data['id'],
413 'number': self.data['number'],
414 'ref': self.patchsets[patchset - 1]['ref'],
415 'revision': self.patchsets[patchset - 1]['revision']
416 }
417 needed.append(d)
418 other.data['neededBy'] = needed
419
420 def query(self):
421 self.queried += 1
422 d = self.data.get('dependsOn')
423 if d:
424 d = d[0]
425 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
426 d['isCurrentPatchSet'] = True
427 else:
428 d['isCurrentPatchSet'] = False
429 return json.loads(json.dumps(self.data))
430
431 def setMerged(self):
432 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000433 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700434 return
435 if self.fail_merge:
436 return
437 self.data['status'] = 'MERGED'
438 self.open = False
439
440 path = os.path.join(self.upstream_root, self.project)
441 repo = git.Repo(path)
442 repo.heads[self.branch].commit = \
443 repo.commit(self.patchsets[-1]['revision'])
444
445 def setReported(self):
446 self.reported += 1
447
448
James E. Blaire511d2f2016-12-08 15:22:26 -0800449class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700450 """A Fake Gerrit connection for use in tests.
451
452 This subclasses
453 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
454 ability for tests to add changes to the fake Gerrit it represents.
455 """
456
Joshua Hesketh352264b2015-08-11 23:42:08 +1000457 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000462 connection_config)
463
James E. Blair7fc8daa2016-08-08 15:37:15 -0700464 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700465 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
466 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700468 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200469 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700470
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700471 def addFakeChange(self, project, branch, subject, status='NEW',
472 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700473 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700475 c = FakeGerritChange(self, self.change_number, project, branch,
476 subject, upstream_root=self.upstream_root,
477 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700478 self.changes[self.change_number] = c
479 return c
480
Clark Boylanb640e052014-04-03 16:41:46 -0700481 def review(self, project, changeid, message, action):
482 number, ps = changeid.split(',')
483 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000484
485 # Add the approval back onto the change (ie simulate what gerrit would
486 # do).
487 # Usually when zuul leaves a review it'll create a feedback loop where
488 # zuul's review enters another gerrit event (which is then picked up by
489 # zuul). However, we can't mimic this behaviour (by adding this
490 # approval event into the queue) as it stops jobs from checking what
491 # happens before this event is triggered. If a job needs to see what
492 # happens they can add their own verified event into the queue.
493 # Nevertheless, we can update change with the new review in gerrit.
494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 for cat in action.keys():
496 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000497 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
Clark Boylanb640e052014-04-03 16:41:46 -0700499 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 if 'submit' in action:
502 change.setMerged()
503 if message:
504 change.setReported()
505
506 def query(self, number):
507 change = self.changes.get(int(number))
508 if change:
509 return change.query()
510 return {}
511
James E. Blairc494d542014-08-06 09:23:52 -0700512 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700513 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700514 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800515 if query.startswith('change:'):
516 # Query a specific changeid
517 changeid = query[len('change:'):]
518 l = [change.query() for change in self.changes.values()
519 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700520 elif query.startswith('message:'):
521 # Query the content of a commit message
522 msg = query[len('message:'):].strip()
523 l = [change.query() for change in self.changes.values()
524 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800525 else:
526 # Query all open changes
527 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700528 return l
James E. Blairc494d542014-08-06 09:23:52 -0700529
Joshua Hesketh352264b2015-08-11 23:42:08 +1000530 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700531 pass
532
Joshua Hesketh352264b2015-08-11 23:42:08 +1000533 def getGitUrl(self, project):
534 return os.path.join(self.upstream_root, project.name)
535
Clark Boylanb640e052014-04-03 16:41:46 -0700536
Gregory Haynes4fc12542015-04-22 20:38:06 -0700537class GithubChangeReference(git.Reference):
538 _common_path_default = "refs/pull"
539 _points_to_commits_only = True
540
541
542class FakeGithubPullRequest(object):
543
544 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100545 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700546 """Creates a new PR with several commits.
547 Sends an event about opened PR."""
548 self.github = github
549 self.source = github
550 self.number = number
551 self.project = project
552 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100553 self.subject = subject
554 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700555 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100556 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700557 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100558 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100559 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.updated_at = None
561 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100562 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100563 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100565 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700566 self._updateTimeStamp()
567
Jan Hruban570d01c2016-03-10 21:51:32 +0100568 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100572 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700573
Jan Hruban570d01c2016-03-10 21:51:32 +0100574 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700575 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100576 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700577 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100578 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579
580 def getPullRequestOpenedEvent(self):
581 return self._getPullRequestEvent('opened')
582
583 def getPullRequestSynchronizeEvent(self):
584 return self._getPullRequestEvent('synchronize')
585
586 def getPullRequestReopenedEvent(self):
587 return self._getPullRequestEvent('reopened')
588
589 def getPullRequestClosedEvent(self):
590 return self._getPullRequestEvent('closed')
591
592 def addComment(self, message):
593 self.comments.append(message)
594 self._updateTimeStamp()
595
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200596 def getCommentAddedEvent(self, text):
597 name = 'issue_comment'
598 data = {
599 'action': 'created',
600 'issue': {
601 'number': self.number
602 },
603 'comment': {
604 'body': text
605 },
606 'repository': {
607 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100608 },
609 'sender': {
610 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200611 }
612 }
613 return (name, data)
614
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800615 def getReviewAddedEvent(self, review):
616 name = 'pull_request_review'
617 data = {
618 'action': 'submitted',
619 'pull_request': {
620 'number': self.number,
621 'title': self.subject,
622 'updated_at': self.updated_at,
623 'base': {
624 'ref': self.branch,
625 'repo': {
626 'full_name': self.project
627 }
628 },
629 'head': {
630 'sha': self.head_sha
631 }
632 },
633 'review': {
634 'state': review
635 },
636 'repository': {
637 'full_name': self.project
638 },
639 'sender': {
640 'login': 'ghuser'
641 }
642 }
643 return (name, data)
644
Jan Hruban16ad31f2015-11-07 14:39:07 +0100645 def addLabel(self, name):
646 if name not in self.labels:
647 self.labels.append(name)
648 self._updateTimeStamp()
649 return self._getLabelEvent(name)
650
651 def removeLabel(self, name):
652 if name in self.labels:
653 self.labels.remove(name)
654 self._updateTimeStamp()
655 return self._getUnlabelEvent(name)
656
657 def _getLabelEvent(self, label):
658 name = 'pull_request'
659 data = {
660 'action': 'labeled',
661 'pull_request': {
662 'number': self.number,
663 'updated_at': self.updated_at,
664 'base': {
665 'ref': self.branch,
666 'repo': {
667 'full_name': self.project
668 }
669 },
670 'head': {
671 'sha': self.head_sha
672 }
673 },
674 'label': {
675 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100676 },
677 'sender': {
678 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100679 }
680 }
681 return (name, data)
682
683 def _getUnlabelEvent(self, label):
684 name = 'pull_request'
685 data = {
686 'action': 'unlabeled',
687 'pull_request': {
688 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100689 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100690 'updated_at': self.updated_at,
691 'base': {
692 'ref': self.branch,
693 'repo': {
694 'full_name': self.project
695 }
696 },
697 'head': {
698 'sha': self.head_sha
699 }
700 },
701 'label': {
702 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100703 },
704 'sender': {
705 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100706 }
707 }
708 return (name, data)
709
Gregory Haynes4fc12542015-04-22 20:38:06 -0700710 def _getRepo(self):
711 repo_path = os.path.join(self.upstream_root, self.project)
712 return git.Repo(repo_path)
713
714 def _createPRRef(self):
715 repo = self._getRepo()
716 GithubChangeReference.create(
717 repo, self._getPRReference(), 'refs/tags/init')
718
Jan Hruban570d01c2016-03-10 21:51:32 +0100719 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700720 repo = self._getRepo()
721 ref = repo.references[self._getPRReference()]
722 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100723 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700724 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100725 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700726 repo.head.reference = ref
727 zuul.merger.merger.reset_repo_to_head(repo)
728 repo.git.clean('-x', '-f', '-d')
729
Jan Hruban570d01c2016-03-10 21:51:32 +0100730 if files:
731 fn = files[0]
732 self.files = files
733 else:
734 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
735 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100736 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700737 fn = os.path.join(repo.working_dir, fn)
738 f = open(fn, 'w')
739 with open(fn, 'w') as f:
740 f.write("test %s %s\n" %
741 (self.branch, self.number))
742 repo.index.add([fn])
743
744 self.head_sha = repo.index.commit(msg).hexsha
745 repo.head.reference = 'master'
746 zuul.merger.merger.reset_repo_to_head(repo)
747 repo.git.clean('-x', '-f', '-d')
748 repo.heads['master'].checkout()
749
750 def _updateTimeStamp(self):
751 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
752
753 def getPRHeadSha(self):
754 repo = self._getRepo()
755 return repo.references[self._getPRReference()].commit.hexsha
756
Jan Hrubane252a732017-01-03 15:03:09 +0100757 def setStatus(self, state, url, description, context):
758 self.statuses[context] = {
759 'state': state,
760 'url': url,
761 'description': description
762 }
763
764 def _clearStatuses(self):
765 self.statuses = {}
766
Gregory Haynes4fc12542015-04-22 20:38:06 -0700767 def _getPRReference(self):
768 return '%s/head' % self.number
769
770 def _getPullRequestEvent(self, action):
771 name = 'pull_request'
772 data = {
773 'action': action,
774 'number': self.number,
775 'pull_request': {
776 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100777 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700778 'updated_at': self.updated_at,
779 'base': {
780 'ref': self.branch,
781 'repo': {
782 'full_name': self.project
783 }
784 },
785 'head': {
786 'sha': self.head_sha
787 }
Jan Hruban3b415922016-02-03 13:10:22 +0100788 },
789 'sender': {
790 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700791 }
792 }
793 return (name, data)
794
795
796class FakeGithubConnection(githubconnection.GithubConnection):
797 log = logging.getLogger("zuul.test.FakeGithubConnection")
798
799 def __init__(self, driver, connection_name, connection_config,
800 upstream_root=None):
801 super(FakeGithubConnection, self).__init__(driver, connection_name,
802 connection_config)
803 self.connection_name = connection_name
804 self.pr_number = 0
805 self.pull_requests = []
806 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100807 self.merge_failure = False
808 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700809
Jan Hruban570d01c2016-03-10 21:51:32 +0100810 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700811 self.pr_number += 1
812 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100813 self, self.pr_number, project, branch, subject, self.upstream_root,
814 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700815 self.pull_requests.append(pull_request)
816 return pull_request
817
Wayne1a78c612015-06-11 17:14:13 -0700818 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
819 if not old_rev:
820 old_rev = '00000000000000000000000000000000'
821 if not new_rev:
822 new_rev = random_sha1()
823 name = 'push'
824 data = {
825 'ref': ref,
826 'before': old_rev,
827 'after': new_rev,
828 'repository': {
829 'full_name': project
830 }
831 }
832 return (name, data)
833
Gregory Haynes4fc12542015-04-22 20:38:06 -0700834 def emitEvent(self, event):
835 """Emulates sending the GitHub webhook event to the connection."""
836 port = self.webapp.server.socket.getsockname()[1]
837 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700838 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 headers = {'X-Github-Event': name}
840 req = urllib.request.Request(
841 'http://localhost:%s/connection/%s/payload'
842 % (port, self.connection_name),
843 data=payload, headers=headers)
844 urllib.request.urlopen(req)
845
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200846 def getPull(self, project, number):
847 pr = self.pull_requests[number - 1]
848 data = {
849 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100850 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200851 'updated_at': pr.updated_at,
852 'base': {
853 'repo': {
854 'full_name': pr.project
855 },
856 'ref': pr.branch,
857 },
Jan Hruban37615e52015-11-19 14:30:49 +0100858 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200859 'head': {
860 'sha': pr.head_sha
861 }
862 }
863 return data
864
Jan Hruban570d01c2016-03-10 21:51:32 +0100865 def getPullFileNames(self, project, number):
866 pr = self.pull_requests[number - 1]
867 return pr.files
868
Jan Hruban3b415922016-02-03 13:10:22 +0100869 def getUser(self, login):
870 data = {
871 'username': login,
872 'name': 'Github User',
873 'email': 'github.user@example.com'
874 }
875 return data
876
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 def getGitUrl(self, project):
878 return os.path.join(self.upstream_root, str(project))
879
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200880 def real_getGitUrl(self, project):
881 return super(FakeGithubConnection, self).getGitUrl(project)
882
Gregory Haynes4fc12542015-04-22 20:38:06 -0700883 def getProjectBranches(self, project):
884 """Masks getProjectBranches since we don't have a real github"""
885
886 # just returns master for now
887 return ['master']
888
Jan Hrubane252a732017-01-03 15:03:09 +0100889 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700890 pull_request = self.pull_requests[pr_number - 1]
891 pull_request.addComment(message)
892
Jan Hruban3b415922016-02-03 13:10:22 +0100893 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100894 pull_request = self.pull_requests[pr_number - 1]
895 if self.merge_failure:
896 raise Exception('Pull request was not merged')
897 if self.merge_not_allowed_count > 0:
898 self.merge_not_allowed_count -= 1
899 raise MergeFailure('Merge was not successful due to mergeability'
900 ' conflict')
901 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100902 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100903
Jan Hrubane252a732017-01-03 15:03:09 +0100904 def setCommitStatus(self, project, sha, state,
905 url='', description='', context=''):
906 owner, proj = project.split('/')
907 for pr in self.pull_requests:
908 pr_owner, pr_project = pr.project.split('/')
909 if (pr_owner == owner and pr_project == proj and
910 pr.head_sha == sha):
911 pr.setStatus(state, url, description, context)
912
Jan Hruban16ad31f2015-11-07 14:39:07 +0100913 def labelPull(self, project, pr_number, label):
914 pull_request = self.pull_requests[pr_number - 1]
915 pull_request.addLabel(label)
916
917 def unlabelPull(self, project, pr_number, label):
918 pull_request = self.pull_requests[pr_number - 1]
919 pull_request.removeLabel(label)
920
Gregory Haynes4fc12542015-04-22 20:38:06 -0700921
Clark Boylanb640e052014-04-03 16:41:46 -0700922class BuildHistory(object):
923 def __init__(self, **kw):
924 self.__dict__.update(kw)
925
926 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700927 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
928 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700929
930
931class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200932 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700933 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700934 self.url = url
935
936 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700937 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700938 path = res.path
939 project = '/'.join(path.split('/')[2:-2])
940 ret = '001e# service=git-upload-pack\n'
941 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
942 'multi_ack thin-pack side-band side-band-64k ofs-delta '
943 'shallow no-progress include-tag multi_ack_detailed no-done\n')
944 path = os.path.join(self.upstream_root, project)
945 repo = git.Repo(path)
946 for ref in repo.refs:
947 r = ref.object.hexsha + ' ' + ref.path + '\n'
948 ret += '%04x%s' % (len(r) + 4, r)
949 ret += '0000'
950 return ret
951
952
Clark Boylanb640e052014-04-03 16:41:46 -0700953class FakeStatsd(threading.Thread):
954 def __init__(self):
955 threading.Thread.__init__(self)
956 self.daemon = True
957 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
958 self.sock.bind(('', 0))
959 self.port = self.sock.getsockname()[1]
960 self.wake_read, self.wake_write = os.pipe()
961 self.stats = []
962
963 def run(self):
964 while True:
965 poll = select.poll()
966 poll.register(self.sock, select.POLLIN)
967 poll.register(self.wake_read, select.POLLIN)
968 ret = poll.poll()
969 for (fd, event) in ret:
970 if fd == self.sock.fileno():
971 data = self.sock.recvfrom(1024)
972 if not data:
973 return
974 self.stats.append(data[0])
975 if fd == self.wake_read:
976 return
977
978 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -0700979 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -0700980
981
James E. Blaire1767bc2016-08-02 10:00:27 -0700982class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700983 log = logging.getLogger("zuul.test")
984
Paul Belanger174a8272017-03-14 13:20:10 -0400985 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700986 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400987 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700988 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700989 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700990 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700991 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700992 # TODOv3(jeblair): self.node is really "the image of the node
993 # assigned". We should rename it (self.node_image?) if we
994 # keep using it like this, or we may end up exposing more of
995 # the complexity around multi-node jobs here
996 # (self.nodes[0].image?)
997 self.node = None
998 if len(self.parameters.get('nodes')) == 1:
999 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001000 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001001 self.pipeline = self.parameters['ZUUL_PIPELINE']
1002 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001003 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001004 self.wait_condition = threading.Condition()
1005 self.waiting = False
1006 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001007 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001008 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001009 self.changes = None
1010 if 'ZUUL_CHANGE_IDS' in self.parameters:
1011 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001012
James E. Blair3158e282016-08-19 09:34:11 -07001013 def __repr__(self):
1014 waiting = ''
1015 if self.waiting:
1016 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001017 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1018 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001019
Clark Boylanb640e052014-04-03 16:41:46 -07001020 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001021 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001022 self.wait_condition.acquire()
1023 self.wait_condition.notify()
1024 self.waiting = False
1025 self.log.debug("Build %s released" % self.unique)
1026 self.wait_condition.release()
1027
1028 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001029 """Return whether this build is being held.
1030
1031 :returns: Whether the build is being held.
1032 :rtype: bool
1033 """
1034
Clark Boylanb640e052014-04-03 16:41:46 -07001035 self.wait_condition.acquire()
1036 if self.waiting:
1037 ret = True
1038 else:
1039 ret = False
1040 self.wait_condition.release()
1041 return ret
1042
1043 def _wait(self):
1044 self.wait_condition.acquire()
1045 self.waiting = True
1046 self.log.debug("Build %s waiting" % self.unique)
1047 self.wait_condition.wait()
1048 self.wait_condition.release()
1049
1050 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001051 self.log.debug('Running build %s' % self.unique)
1052
Paul Belanger174a8272017-03-14 13:20:10 -04001053 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001054 self.log.debug('Holding build %s' % self.unique)
1055 self._wait()
1056 self.log.debug("Build %s continuing" % self.unique)
1057
James E. Blair412fba82017-01-26 15:00:50 -08001058 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001059 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001060 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001061 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001062 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001063 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001064 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001065
James E. Blaire1767bc2016-08-02 10:00:27 -07001066 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001067
James E. Blaira5dba232016-08-08 15:53:24 -07001068 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001069 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001070 for change in changes:
1071 if self.hasChanges(change):
1072 return True
1073 return False
1074
James E. Blaire7b99a02016-08-05 14:27:34 -07001075 def hasChanges(self, *changes):
1076 """Return whether this build has certain changes in its git repos.
1077
1078 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001079 are expected to be present (in order) in the git repository of
1080 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001081
1082 :returns: Whether the build has the indicated changes.
1083 :rtype: bool
1084
1085 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001086 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001087 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001088 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001089 try:
1090 repo = git.Repo(path)
1091 except NoSuchPathError as e:
1092 self.log.debug('%s' % e)
1093 return False
1094 ref = self.parameters['ZUUL_REF']
1095 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1096 commit_message = '%s-1' % change.subject
1097 self.log.debug("Checking if build %s has changes; commit_message "
1098 "%s; repo_messages %s" % (self, commit_message,
1099 repo_messages))
1100 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001101 self.log.debug(" messages do not match")
1102 return False
1103 self.log.debug(" OK")
1104 return True
1105
Clark Boylanb640e052014-04-03 16:41:46 -07001106
Paul Belanger174a8272017-03-14 13:20:10 -04001107class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1108 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001109
Paul Belanger174a8272017-03-14 13:20:10 -04001110 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001111 they will report that they have started but then pause until
1112 released before reporting completion. This attribute may be
1113 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001114 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001115 be explicitly released.
1116
1117 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001118 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001119 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001120 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001121 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001122 self.hold_jobs_in_build = False
1123 self.lock = threading.Lock()
1124 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001125 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001126 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001127 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001128
James E. Blaira5dba232016-08-08 15:53:24 -07001129 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001130 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001131
1132 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001133 :arg Change change: The :py:class:`~tests.base.FakeChange`
1134 instance which should cause the job to fail. This job
1135 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001136
1137 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001138 l = self.fail_tests.get(name, [])
1139 l.append(change)
1140 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001141
James E. Blair962220f2016-08-03 11:22:38 -07001142 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001143 """Release a held build.
1144
1145 :arg str regex: A regular expression which, if supplied, will
1146 cause only builds with matching names to be released. If
1147 not supplied, all builds will be released.
1148
1149 """
James E. Blair962220f2016-08-03 11:22:38 -07001150 builds = self.running_builds[:]
1151 self.log.debug("Releasing build %s (%s)" % (regex,
1152 len(self.running_builds)))
1153 for build in builds:
1154 if not regex or re.match(regex, build.name):
1155 self.log.debug("Releasing build %s" %
1156 (build.parameters['ZUUL_UUID']))
1157 build.release()
1158 else:
1159 self.log.debug("Not releasing build %s" %
1160 (build.parameters['ZUUL_UUID']))
1161 self.log.debug("Done releasing builds %s (%s)" %
1162 (regex, len(self.running_builds)))
1163
Paul Belanger174a8272017-03-14 13:20:10 -04001164 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001165 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001166 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001167 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001168 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001169 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001170 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001171 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001172 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1173 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001174
1175 def stopJob(self, job):
1176 self.log.debug("handle stop")
1177 parameters = json.loads(job.arguments)
1178 uuid = parameters['uuid']
1179 for build in self.running_builds:
1180 if build.unique == uuid:
1181 build.aborted = True
1182 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001183 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001184
James E. Blaira002b032017-04-18 10:35:48 -07001185 def stop(self):
1186 for build in self.running_builds:
1187 build.release()
1188 super(RecordingExecutorServer, self).stop()
1189
Joshua Hesketh50c21782016-10-13 21:34:14 +11001190
Paul Belanger174a8272017-03-14 13:20:10 -04001191class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001192 def doMergeChanges(self, items):
1193 # Get a merger in order to update the repos involved in this job.
1194 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1195 if not commit: # merge conflict
1196 self.recordResult('MERGER_FAILURE')
1197 return commit
1198
1199 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001200 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001201 self.executor_server.lock.acquire()
1202 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001203 BuildHistory(name=build.name, result=result, changes=build.changes,
1204 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001205 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001206 pipeline=build.parameters['ZUUL_PIPELINE'])
1207 )
Paul Belanger174a8272017-03-14 13:20:10 -04001208 self.executor_server.running_builds.remove(build)
1209 del self.executor_server.job_builds[self.job.unique]
1210 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001211
1212 def runPlaybooks(self, args):
1213 build = self.executor_server.job_builds[self.job.unique]
1214 build.jobdir = self.jobdir
1215
1216 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1217 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001218 return result
1219
Monty Taylore6562aa2017-02-20 07:37:39 -05001220 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001221 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001222
Paul Belanger174a8272017-03-14 13:20:10 -04001223 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001224 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001225 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001226 else:
1227 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001228 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001229
James E. Blairad8dca02017-02-21 11:48:32 -05001230 def getHostList(self, args):
1231 self.log.debug("hostlist")
1232 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001233 for host in hosts:
1234 host['host_vars']['ansible_connection'] = 'local'
1235
1236 hosts.append(dict(
1237 name='localhost',
1238 host_vars=dict(ansible_connection='local'),
1239 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001240 return hosts
1241
James E. Blairf5dbd002015-12-23 15:26:17 -08001242
Clark Boylanb640e052014-04-03 16:41:46 -07001243class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001244 """A Gearman server for use in tests.
1245
1246 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1247 added to the queue but will not be distributed to workers
1248 until released. This attribute may be changed at any time and
1249 will take effect for subsequently enqueued jobs, but
1250 previously held jobs will still need to be explicitly
1251 released.
1252
1253 """
1254
Clark Boylanb640e052014-04-03 16:41:46 -07001255 def __init__(self):
1256 self.hold_jobs_in_queue = False
1257 super(FakeGearmanServer, self).__init__(0)
1258
1259 def getJobForConnection(self, connection, peek=False):
1260 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1261 for job in queue:
1262 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001263 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001264 job.waiting = self.hold_jobs_in_queue
1265 else:
1266 job.waiting = False
1267 if job.waiting:
1268 continue
1269 if job.name in connection.functions:
1270 if not peek:
1271 queue.remove(job)
1272 connection.related_jobs[job.handle] = job
1273 job.worker_connection = connection
1274 job.running = True
1275 return job
1276 return None
1277
1278 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001279 """Release a held job.
1280
1281 :arg str regex: A regular expression which, if supplied, will
1282 cause only jobs with matching names to be released. If
1283 not supplied, all jobs will be released.
1284 """
Clark Boylanb640e052014-04-03 16:41:46 -07001285 released = False
1286 qlen = (len(self.high_queue) + len(self.normal_queue) +
1287 len(self.low_queue))
1288 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1289 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001290 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001291 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001292 parameters = json.loads(job.arguments)
1293 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001294 self.log.debug("releasing queued job %s" %
1295 job.unique)
1296 job.waiting = False
1297 released = True
1298 else:
1299 self.log.debug("not releasing queued job %s" %
1300 job.unique)
1301 if released:
1302 self.wakeConnections()
1303 qlen = (len(self.high_queue) + len(self.normal_queue) +
1304 len(self.low_queue))
1305 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1306
1307
1308class FakeSMTP(object):
1309 log = logging.getLogger('zuul.FakeSMTP')
1310
1311 def __init__(self, messages, server, port):
1312 self.server = server
1313 self.port = port
1314 self.messages = messages
1315
1316 def sendmail(self, from_email, to_email, msg):
1317 self.log.info("Sending email from %s, to %s, with msg %s" % (
1318 from_email, to_email, msg))
1319
1320 headers = msg.split('\n\n', 1)[0]
1321 body = msg.split('\n\n', 1)[1]
1322
1323 self.messages.append(dict(
1324 from_email=from_email,
1325 to_email=to_email,
1326 msg=msg,
1327 headers=headers,
1328 body=body,
1329 ))
1330
1331 return True
1332
1333 def quit(self):
1334 return True
1335
1336
James E. Blairdce6cea2016-12-20 16:45:32 -08001337class FakeNodepool(object):
1338 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001339 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001340
1341 log = logging.getLogger("zuul.test.FakeNodepool")
1342
1343 def __init__(self, host, port, chroot):
1344 self.client = kazoo.client.KazooClient(
1345 hosts='%s:%s%s' % (host, port, chroot))
1346 self.client.start()
1347 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001348 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001349 self.thread = threading.Thread(target=self.run)
1350 self.thread.daemon = True
1351 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001352 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001353
1354 def stop(self):
1355 self._running = False
1356 self.thread.join()
1357 self.client.stop()
1358 self.client.close()
1359
1360 def run(self):
1361 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001362 try:
1363 self._run()
1364 except Exception:
1365 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001366 time.sleep(0.1)
1367
1368 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001369 if self.paused:
1370 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001371 for req in self.getNodeRequests():
1372 self.fulfillRequest(req)
1373
1374 def getNodeRequests(self):
1375 try:
1376 reqids = self.client.get_children(self.REQUEST_ROOT)
1377 except kazoo.exceptions.NoNodeError:
1378 return []
1379 reqs = []
1380 for oid in sorted(reqids):
1381 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001382 try:
1383 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001384 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001385 data['_oid'] = oid
1386 reqs.append(data)
1387 except kazoo.exceptions.NoNodeError:
1388 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001389 return reqs
1390
James E. Blaire18d4602017-01-05 11:17:28 -08001391 def getNodes(self):
1392 try:
1393 nodeids = self.client.get_children(self.NODE_ROOT)
1394 except kazoo.exceptions.NoNodeError:
1395 return []
1396 nodes = []
1397 for oid in sorted(nodeids):
1398 path = self.NODE_ROOT + '/' + oid
1399 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001400 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001401 data['_oid'] = oid
1402 try:
1403 lockfiles = self.client.get_children(path + '/lock')
1404 except kazoo.exceptions.NoNodeError:
1405 lockfiles = []
1406 if lockfiles:
1407 data['_lock'] = True
1408 else:
1409 data['_lock'] = False
1410 nodes.append(data)
1411 return nodes
1412
James E. Blaira38c28e2017-01-04 10:33:20 -08001413 def makeNode(self, request_id, node_type):
1414 now = time.time()
1415 path = '/nodepool/nodes/'
1416 data = dict(type=node_type,
1417 provider='test-provider',
1418 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001419 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001420 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001421 public_ipv4='127.0.0.1',
1422 private_ipv4=None,
1423 public_ipv6=None,
1424 allocated_to=request_id,
1425 state='ready',
1426 state_time=now,
1427 created_time=now,
1428 updated_time=now,
1429 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001430 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001431 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001432 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001433 path = self.client.create(path, data,
1434 makepath=True,
1435 sequence=True)
1436 nodeid = path.split("/")[-1]
1437 return nodeid
1438
James E. Blair6ab79e02017-01-06 10:10:17 -08001439 def addFailRequest(self, request):
1440 self.fail_requests.add(request['_oid'])
1441
James E. Blairdce6cea2016-12-20 16:45:32 -08001442 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001443 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001444 return
1445 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001446 oid = request['_oid']
1447 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001448
James E. Blair6ab79e02017-01-06 10:10:17 -08001449 if oid in self.fail_requests:
1450 request['state'] = 'failed'
1451 else:
1452 request['state'] = 'fulfilled'
1453 nodes = []
1454 for node in request['node_types']:
1455 nodeid = self.makeNode(oid, node)
1456 nodes.append(nodeid)
1457 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001458
James E. Blaira38c28e2017-01-04 10:33:20 -08001459 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001460 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001461 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001462 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001463 try:
1464 self.client.set(path, data)
1465 except kazoo.exceptions.NoNodeError:
1466 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001467
1468
James E. Blair498059b2016-12-20 13:50:13 -08001469class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001470 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001471 super(ChrootedKazooFixture, self).__init__()
1472
1473 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1474 if ':' in zk_host:
1475 host, port = zk_host.split(':')
1476 else:
1477 host = zk_host
1478 port = None
1479
1480 self.zookeeper_host = host
1481
1482 if not port:
1483 self.zookeeper_port = 2181
1484 else:
1485 self.zookeeper_port = int(port)
1486
Clark Boylan621ec9a2017-04-07 17:41:33 -07001487 self.test_id = test_id
1488
James E. Blair498059b2016-12-20 13:50:13 -08001489 def _setUp(self):
1490 # Make sure the test chroot paths do not conflict
1491 random_bits = ''.join(random.choice(string.ascii_lowercase +
1492 string.ascii_uppercase)
1493 for x in range(8))
1494
Clark Boylan621ec9a2017-04-07 17:41:33 -07001495 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001496 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1497
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001498 self.addCleanup(self._cleanup)
1499
James E. Blair498059b2016-12-20 13:50:13 -08001500 # Ensure the chroot path exists and clean up any pre-existing znodes.
1501 _tmp_client = kazoo.client.KazooClient(
1502 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1503 _tmp_client.start()
1504
1505 if _tmp_client.exists(self.zookeeper_chroot):
1506 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1507
1508 _tmp_client.ensure_path(self.zookeeper_chroot)
1509 _tmp_client.stop()
1510 _tmp_client.close()
1511
James E. Blair498059b2016-12-20 13:50:13 -08001512 def _cleanup(self):
1513 '''Remove the chroot path.'''
1514 # Need a non-chroot'ed client to remove the chroot path
1515 _tmp_client = kazoo.client.KazooClient(
1516 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1517 _tmp_client.start()
1518 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1519 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001520 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001521
1522
Joshua Heskethd78b4482015-09-14 16:56:34 -06001523class MySQLSchemaFixture(fixtures.Fixture):
1524 def setUp(self):
1525 super(MySQLSchemaFixture, self).setUp()
1526
1527 random_bits = ''.join(random.choice(string.ascii_lowercase +
1528 string.ascii_uppercase)
1529 for x in range(8))
1530 self.name = '%s_%s' % (random_bits, os.getpid())
1531 self.passwd = uuid.uuid4().hex
1532 db = pymysql.connect(host="localhost",
1533 user="openstack_citest",
1534 passwd="openstack_citest",
1535 db="openstack_citest")
1536 cur = db.cursor()
1537 cur.execute("create database %s" % self.name)
1538 cur.execute(
1539 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1540 (self.name, self.name, self.passwd))
1541 cur.execute("flush privileges")
1542
1543 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1544 self.passwd,
1545 self.name)
1546 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1547 self.addCleanup(self.cleanup)
1548
1549 def cleanup(self):
1550 db = pymysql.connect(host="localhost",
1551 user="openstack_citest",
1552 passwd="openstack_citest",
1553 db="openstack_citest")
1554 cur = db.cursor()
1555 cur.execute("drop database %s" % self.name)
1556 cur.execute("drop user '%s'@'localhost'" % self.name)
1557 cur.execute("flush privileges")
1558
1559
Maru Newby3fe5f852015-01-13 04:22:14 +00001560class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001561 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001562 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001563
James E. Blair1c236df2017-02-01 14:07:24 -08001564 def attachLogs(self, *args):
1565 def reader():
1566 self._log_stream.seek(0)
1567 while True:
1568 x = self._log_stream.read(4096)
1569 if not x:
1570 break
1571 yield x.encode('utf8')
1572 content = testtools.content.content_from_reader(
1573 reader,
1574 testtools.content_type.UTF8_TEXT,
1575 False)
1576 self.addDetail('logging', content)
1577
Clark Boylanb640e052014-04-03 16:41:46 -07001578 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001579 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001580 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1581 try:
1582 test_timeout = int(test_timeout)
1583 except ValueError:
1584 # If timeout value is invalid do not set a timeout.
1585 test_timeout = 0
1586 if test_timeout > 0:
1587 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1588
1589 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1590 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1591 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1592 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1593 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1594 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1595 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1596 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1597 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1598 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001599 self._log_stream = StringIO()
1600 self.addOnException(self.attachLogs)
1601 else:
1602 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001603
James E. Blair1c236df2017-02-01 14:07:24 -08001604 handler = logging.StreamHandler(self._log_stream)
1605 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1606 '%(levelname)-8s %(message)s')
1607 handler.setFormatter(formatter)
1608
1609 logger = logging.getLogger()
1610 logger.setLevel(logging.DEBUG)
1611 logger.addHandler(handler)
1612
Clark Boylan3410d532017-04-25 12:35:29 -07001613 # Make sure we don't carry old handlers around in process state
1614 # which slows down test runs
1615 self.addCleanup(logger.removeHandler, handler)
1616 self.addCleanup(handler.close)
1617 self.addCleanup(handler.flush)
1618
James E. Blair1c236df2017-02-01 14:07:24 -08001619 # NOTE(notmorgan): Extract logging overrides for specific
1620 # libraries from the OS_LOG_DEFAULTS env and create loggers
1621 # for each. This is used to limit the output during test runs
1622 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001623 log_defaults_from_env = os.environ.get(
1624 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001625 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001626
James E. Blairdce6cea2016-12-20 16:45:32 -08001627 if log_defaults_from_env:
1628 for default in log_defaults_from_env.split(','):
1629 try:
1630 name, level_str = default.split('=', 1)
1631 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001632 logger = logging.getLogger(name)
1633 logger.setLevel(level)
1634 logger.addHandler(handler)
1635 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001636 except ValueError:
1637 # NOTE(notmorgan): Invalid format of the log default,
1638 # skip and don't try and apply a logger for the
1639 # specified module
1640 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001641
Maru Newby3fe5f852015-01-13 04:22:14 +00001642
1643class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001644 """A test case with a functioning Zuul.
1645
1646 The following class variables are used during test setup and can
1647 be overidden by subclasses but are effectively read-only once a
1648 test method starts running:
1649
1650 :cvar str config_file: This points to the main zuul config file
1651 within the fixtures directory. Subclasses may override this
1652 to obtain a different behavior.
1653
1654 :cvar str tenant_config_file: This is the tenant config file
1655 (which specifies from what git repos the configuration should
1656 be loaded). It defaults to the value specified in
1657 `config_file` but can be overidden by subclasses to obtain a
1658 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001659 configuration. See also the :py:func:`simple_layout`
1660 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001661
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001662 :cvar bool create_project_keys: Indicates whether Zuul should
1663 auto-generate keys for each project, or whether the test
1664 infrastructure should insert dummy keys to save time during
1665 startup. Defaults to False.
1666
James E. Blaire7b99a02016-08-05 14:27:34 -07001667 The following are instance variables that are useful within test
1668 methods:
1669
1670 :ivar FakeGerritConnection fake_<connection>:
1671 A :py:class:`~tests.base.FakeGerritConnection` will be
1672 instantiated for each connection present in the config file
1673 and stored here. For instance, `fake_gerrit` will hold the
1674 FakeGerritConnection object for a connection named `gerrit`.
1675
1676 :ivar FakeGearmanServer gearman_server: An instance of
1677 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1678 server that all of the Zuul components in this test use to
1679 communicate with each other.
1680
Paul Belanger174a8272017-03-14 13:20:10 -04001681 :ivar RecordingExecutorServer executor_server: An instance of
1682 :py:class:`~tests.base.RecordingExecutorServer` which is the
1683 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001684
1685 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1686 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001687 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001688 list upon completion.
1689
1690 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1691 objects representing completed builds. They are appended to
1692 the list in the order they complete.
1693
1694 """
1695
James E. Blair83005782015-12-11 14:46:03 -08001696 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001697 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001698 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001699
1700 def _startMerger(self):
1701 self.merge_server = zuul.merger.server.MergeServer(self.config,
1702 self.connections)
1703 self.merge_server.start()
1704
Maru Newby3fe5f852015-01-13 04:22:14 +00001705 def setUp(self):
1706 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001707
1708 self.setupZK()
1709
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001710 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001711 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001712 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1713 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001714 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001715 tmp_root = tempfile.mkdtemp(
1716 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001717 self.test_root = os.path.join(tmp_root, "zuul-test")
1718 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001719 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001720 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001721 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001722
1723 if os.path.exists(self.test_root):
1724 shutil.rmtree(self.test_root)
1725 os.makedirs(self.test_root)
1726 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001727 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001728
1729 # Make per test copy of Configuration.
1730 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001731 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001732 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001733 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001734 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001735 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001736 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001737
Clark Boylanb640e052014-04-03 16:41:46 -07001738 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001739 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1740 # see: https://github.com/jsocol/pystatsd/issues/61
1741 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001742 os.environ['STATSD_PORT'] = str(self.statsd.port)
1743 self.statsd.start()
1744 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001745 reload_module(statsd)
1746 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001747
1748 self.gearman_server = FakeGearmanServer()
1749
1750 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001751 self.log.info("Gearman server on port %s" %
1752 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001753
James E. Blaire511d2f2016-12-08 15:22:26 -08001754 gerritsource.GerritSource.replication_timeout = 1.5
1755 gerritsource.GerritSource.replication_retry_interval = 0.5
1756 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001757
Joshua Hesketh352264b2015-08-11 23:42:08 +10001758 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001759
Jan Hruban7083edd2015-08-21 14:00:54 +02001760 self.webapp = zuul.webapp.WebApp(
1761 self.sched, port=0, listen_address='127.0.0.1')
1762
Jan Hruban6b71aff2015-10-22 16:58:08 +02001763 self.event_queues = [
1764 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001765 self.sched.trigger_event_queue,
1766 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001767 ]
1768
James E. Blairfef78942016-03-11 16:28:56 -08001769 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001770 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001771
Clark Boylanb640e052014-04-03 16:41:46 -07001772 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001773 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001774 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001775 return FakeURLOpener(self.upstream_root, *args, **kw)
1776
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001777 old_urlopen = urllib.request.urlopen
1778 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001779
James E. Blair3f876d52016-07-22 13:07:14 -07001780 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001781
Paul Belanger174a8272017-03-14 13:20:10 -04001782 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001783 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001784 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001785 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001786 _test_root=self.test_root,
1787 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001788 self.executor_server.start()
1789 self.history = self.executor_server.build_history
1790 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001791
Paul Belanger174a8272017-03-14 13:20:10 -04001792 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001793 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001794 self.merge_client = zuul.merger.client.MergeClient(
1795 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001796 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001797 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001798 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001799
James E. Blair0d5a36e2017-02-21 10:53:44 -05001800 self.fake_nodepool = FakeNodepool(
1801 self.zk_chroot_fixture.zookeeper_host,
1802 self.zk_chroot_fixture.zookeeper_port,
1803 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001804
Paul Belanger174a8272017-03-14 13:20:10 -04001805 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001806 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001807 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001808 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001809
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001810 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001811
1812 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001813 self.webapp.start()
1814 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001815 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001816 # Cleanups are run in reverse order
1817 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001818 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001819 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001820
James E. Blairb9c0d772017-03-03 14:34:49 -08001821 self.sched.reconfigure(self.config)
1822 self.sched.resume()
1823
James E. Blairfef78942016-03-11 16:28:56 -08001824 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001825 # Set up gerrit related fakes
1826 # Set a changes database so multiple FakeGerrit's can report back to
1827 # a virtual canonical database given by the configured hostname
1828 self.gerrit_changes_dbs = {}
1829
1830 def getGerritConnection(driver, name, config):
1831 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1832 con = FakeGerritConnection(driver, name, config,
1833 changes_db=db,
1834 upstream_root=self.upstream_root)
1835 self.event_queues.append(con.event_queue)
1836 setattr(self, 'fake_' + name, con)
1837 return con
1838
1839 self.useFixture(fixtures.MonkeyPatch(
1840 'zuul.driver.gerrit.GerritDriver.getConnection',
1841 getGerritConnection))
1842
Gregory Haynes4fc12542015-04-22 20:38:06 -07001843 def getGithubConnection(driver, name, config):
1844 con = FakeGithubConnection(driver, name, config,
1845 upstream_root=self.upstream_root)
1846 setattr(self, 'fake_' + name, con)
1847 return con
1848
1849 self.useFixture(fixtures.MonkeyPatch(
1850 'zuul.driver.github.GithubDriver.getConnection',
1851 getGithubConnection))
1852
James E. Blaire511d2f2016-12-08 15:22:26 -08001853 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001854 # TODO(jhesketh): This should come from lib.connections for better
1855 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001856 # Register connections from the config
1857 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001858
Joshua Hesketh352264b2015-08-11 23:42:08 +10001859 def FakeSMTPFactory(*args, **kw):
1860 args = [self.smtp_messages] + list(args)
1861 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001862
Joshua Hesketh352264b2015-08-11 23:42:08 +10001863 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001864
James E. Blaire511d2f2016-12-08 15:22:26 -08001865 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001866 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001867 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001868
James E. Blair83005782015-12-11 14:46:03 -08001869 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001870 # This creates the per-test configuration object. It can be
1871 # overriden by subclasses, but should not need to be since it
1872 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001873 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001874 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001875
1876 if not self.setupSimpleLayout():
1877 if hasattr(self, 'tenant_config_file'):
1878 self.config.set('zuul', 'tenant_config',
1879 self.tenant_config_file)
1880 git_path = os.path.join(
1881 os.path.dirname(
1882 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1883 'git')
1884 if os.path.exists(git_path):
1885 for reponame in os.listdir(git_path):
1886 project = reponame.replace('_', '/')
1887 self.copyDirToRepo(project,
1888 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001889 self.setupAllProjectKeys()
1890
James E. Blair06cc3922017-04-19 10:08:10 -07001891 def setupSimpleLayout(self):
1892 # If the test method has been decorated with a simple_layout,
1893 # use that instead of the class tenant_config_file. Set up a
1894 # single config-project with the specified layout, and
1895 # initialize repos for all of the 'project' entries which
1896 # appear in the layout.
1897 test_name = self.id().split('.')[-1]
1898 test = getattr(self, test_name)
1899 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001900 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001901 else:
1902 return False
1903
James E. Blairb70e55a2017-04-19 12:57:02 -07001904 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001905 path = os.path.join(FIXTURE_DIR, path)
1906 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001907 data = f.read()
1908 layout = yaml.safe_load(data)
1909 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001910 untrusted_projects = []
1911 for item in layout:
1912 if 'project' in item:
1913 name = item['project']['name']
1914 untrusted_projects.append(name)
1915 self.init_repo(name)
1916 self.addCommitToRepo(name, 'initial commit',
1917 files={'README': ''},
1918 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001919 if 'job' in item:
1920 jobname = item['job']['name']
1921 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001922
1923 root = os.path.join(self.test_root, "config")
1924 if not os.path.exists(root):
1925 os.makedirs(root)
1926 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1927 config = [{'tenant':
1928 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001929 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001930 {'config-projects': ['common-config'],
1931 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07001932 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07001933 f.close()
1934 self.config.set('zuul', 'tenant_config',
1935 os.path.join(FIXTURE_DIR, f.name))
1936
1937 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001938 self.addCommitToRepo('common-config', 'add content from fixture',
1939 files, branch='master', tag='init')
1940
1941 return True
1942
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001943 def setupAllProjectKeys(self):
1944 if self.create_project_keys:
1945 return
1946
1947 path = self.config.get('zuul', 'tenant_config')
1948 with open(os.path.join(FIXTURE_DIR, path)) as f:
1949 tenant_config = yaml.safe_load(f.read())
1950 for tenant in tenant_config:
1951 sources = tenant['tenant']['source']
1952 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001953 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001954 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001955 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001956 self.setupProjectKeys(source, project)
1957
1958 def setupProjectKeys(self, source, project):
1959 # Make sure we set up an RSA key for the project so that we
1960 # don't spend time generating one:
1961
1962 key_root = os.path.join(self.state_root, 'keys')
1963 if not os.path.isdir(key_root):
1964 os.mkdir(key_root, 0o700)
1965 private_key_file = os.path.join(key_root, source, project + '.pem')
1966 private_key_dir = os.path.dirname(private_key_file)
1967 self.log.debug("Installing test keys for project %s at %s" % (
1968 project, private_key_file))
1969 if not os.path.isdir(private_key_dir):
1970 os.makedirs(private_key_dir)
1971 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1972 with open(private_key_file, 'w') as o:
1973 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001974
James E. Blair498059b2016-12-20 13:50:13 -08001975 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001976 self.zk_chroot_fixture = self.useFixture(
1977 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001978 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001979 self.zk_chroot_fixture.zookeeper_host,
1980 self.zk_chroot_fixture.zookeeper_port,
1981 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001982
James E. Blair96c6bf82016-01-15 16:20:40 -08001983 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001984 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001985
1986 files = {}
1987 for (dirpath, dirnames, filenames) in os.walk(source_path):
1988 for filename in filenames:
1989 test_tree_filepath = os.path.join(dirpath, filename)
1990 common_path = os.path.commonprefix([test_tree_filepath,
1991 source_path])
1992 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1993 with open(test_tree_filepath, 'r') as f:
1994 content = f.read()
1995 files[relative_filepath] = content
1996 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001997 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001998
James E. Blaire18d4602017-01-05 11:17:28 -08001999 def assertNodepoolState(self):
2000 # Make sure that there are no pending requests
2001
2002 requests = self.fake_nodepool.getNodeRequests()
2003 self.assertEqual(len(requests), 0)
2004
2005 nodes = self.fake_nodepool.getNodes()
2006 for node in nodes:
2007 self.assertFalse(node['_lock'], "Node %s is locked" %
2008 (node['_oid'],))
2009
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002010 def assertNoGeneratedKeys(self):
2011 # Make sure that Zuul did not generate any project keys
2012 # (unless it was supposed to).
2013
2014 if self.create_project_keys:
2015 return
2016
2017 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2018 test_key = i.read()
2019
2020 key_root = os.path.join(self.state_root, 'keys')
2021 for root, dirname, files in os.walk(key_root):
2022 for fn in files:
2023 with open(os.path.join(root, fn)) as f:
2024 self.assertEqual(test_key, f.read())
2025
Clark Boylanb640e052014-04-03 16:41:46 -07002026 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002027 self.log.debug("Assert final state")
2028 # Make sure no jobs are running
2029 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002030 # Make sure that git.Repo objects have been garbage collected.
2031 repos = []
2032 gc.collect()
2033 for obj in gc.get_objects():
2034 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08002035 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07002036 repos.append(obj)
2037 self.assertEqual(len(repos), 0)
2038 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002039 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002040 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002041 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002042 for tenant in self.sched.abide.tenants.values():
2043 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002044 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002045 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002046
2047 def shutdown(self):
2048 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002049 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07002050 self.merge_server.stop()
2051 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002052 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002053 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002054 self.sched.stop()
2055 self.sched.join()
2056 self.statsd.stop()
2057 self.statsd.join()
2058 self.webapp.stop()
2059 self.webapp.join()
2060 self.rpc.stop()
2061 self.rpc.join()
2062 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002063 self.fake_nodepool.stop()
2064 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002065 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002066 # we whitelist watchdog threads as they have relatively long delays
2067 # before noticing they should exit, but they should exit on their own.
2068 threads = [t for t in threading.enumerate()
2069 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002070 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002071 log_str = ""
2072 for thread_id, stack_frame in sys._current_frames().items():
2073 log_str += "Thread: %s\n" % thread_id
2074 log_str += "".join(traceback.format_stack(stack_frame))
2075 self.log.debug(log_str)
2076 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002077
James E. Blaira002b032017-04-18 10:35:48 -07002078 def assertCleanShutdown(self):
2079 pass
2080
James E. Blairc4ba97a2017-04-19 16:26:24 -07002081 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002082 parts = project.split('/')
2083 path = os.path.join(self.upstream_root, *parts[:-1])
2084 if not os.path.exists(path):
2085 os.makedirs(path)
2086 path = os.path.join(self.upstream_root, project)
2087 repo = git.Repo.init(path)
2088
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002089 with repo.config_writer() as config_writer:
2090 config_writer.set_value('user', 'email', 'user@example.com')
2091 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002092
Clark Boylanb640e052014-04-03 16:41:46 -07002093 repo.index.commit('initial commit')
2094 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002095 if tag:
2096 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002097
James E. Blair97d902e2014-08-21 13:25:56 -07002098 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002099 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002100 repo.git.clean('-x', '-f', '-d')
2101
James E. Blair97d902e2014-08-21 13:25:56 -07002102 def create_branch(self, project, branch):
2103 path = os.path.join(self.upstream_root, project)
2104 repo = git.Repo.init(path)
2105 fn = os.path.join(path, 'README')
2106
2107 branch_head = repo.create_head(branch)
2108 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002109 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002110 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002111 f.close()
2112 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002113 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002114
James E. Blair97d902e2014-08-21 13:25:56 -07002115 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002116 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002117 repo.git.clean('-x', '-f', '-d')
2118
Sachi King9f16d522016-03-16 12:20:45 +11002119 def create_commit(self, project):
2120 path = os.path.join(self.upstream_root, project)
2121 repo = git.Repo(path)
2122 repo.head.reference = repo.heads['master']
2123 file_name = os.path.join(path, 'README')
2124 with open(file_name, 'a') as f:
2125 f.write('creating fake commit\n')
2126 repo.index.add([file_name])
2127 commit = repo.index.commit('Creating a fake commit')
2128 return commit.hexsha
2129
James E. Blairf4a5f022017-04-18 14:01:10 -07002130 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002131 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002132 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002133 while len(self.builds):
2134 self.release(self.builds[0])
2135 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002136 i += 1
2137 if count is not None and i >= count:
2138 break
James E. Blairb8c16472015-05-05 14:55:26 -07002139
Clark Boylanb640e052014-04-03 16:41:46 -07002140 def release(self, job):
2141 if isinstance(job, FakeBuild):
2142 job.release()
2143 else:
2144 job.waiting = False
2145 self.log.debug("Queued job %s released" % job.unique)
2146 self.gearman_server.wakeConnections()
2147
2148 def getParameter(self, job, name):
2149 if isinstance(job, FakeBuild):
2150 return job.parameters[name]
2151 else:
2152 parameters = json.loads(job.arguments)
2153 return parameters[name]
2154
Clark Boylanb640e052014-04-03 16:41:46 -07002155 def haveAllBuildsReported(self):
2156 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002157 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002158 return False
2159 # Find out if every build that the worker has completed has been
2160 # reported back to Zuul. If it hasn't then that means a Gearman
2161 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002162 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002163 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002164 if not zbuild:
2165 # It has already been reported
2166 continue
2167 # It hasn't been reported yet.
2168 return False
2169 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002170 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002171 if connection.state == 'GRAB_WAIT':
2172 return False
2173 return True
2174
2175 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002176 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002177 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002178 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002179 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002180 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002181 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002182 for j in conn.related_jobs.values():
2183 if j.unique == build.uuid:
2184 client_job = j
2185 break
2186 if not client_job:
2187 self.log.debug("%s is not known to the gearman client" %
2188 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002189 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002190 if not client_job.handle:
2191 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002192 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002193 server_job = self.gearman_server.jobs.get(client_job.handle)
2194 if not server_job:
2195 self.log.debug("%s is not known to the gearman server" %
2196 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002197 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002198 if not hasattr(server_job, 'waiting'):
2199 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002200 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002201 if server_job.waiting:
2202 continue
James E. Blair17302972016-08-10 16:11:42 -07002203 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002204 self.log.debug("%s has not reported start" % build)
2205 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002206 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002207 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002208 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002209 if worker_build:
2210 if worker_build.isWaiting():
2211 continue
2212 else:
2213 self.log.debug("%s is running" % worker_build)
2214 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002215 else:
James E. Blair962220f2016-08-03 11:22:38 -07002216 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002217 return False
James E. Blaira002b032017-04-18 10:35:48 -07002218 for (build_uuid, job_worker) in \
2219 self.executor_server.job_workers.items():
2220 if build_uuid not in seen_builds:
2221 self.log.debug("%s is not finalized" % build_uuid)
2222 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002223 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002224
James E. Blairdce6cea2016-12-20 16:45:32 -08002225 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002226 if self.fake_nodepool.paused:
2227 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002228 if self.sched.nodepool.requests:
2229 return False
2230 return True
2231
Jan Hruban6b71aff2015-10-22 16:58:08 +02002232 def eventQueuesEmpty(self):
2233 for queue in self.event_queues:
2234 yield queue.empty()
2235
2236 def eventQueuesJoin(self):
2237 for queue in self.event_queues:
2238 queue.join()
2239
Clark Boylanb640e052014-04-03 16:41:46 -07002240 def waitUntilSettled(self):
2241 self.log.debug("Waiting until settled...")
2242 start = time.time()
2243 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002244 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002245 self.log.error("Timeout waiting for Zuul to settle")
2246 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002247 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002248 self.log.error(" %s: %s" % (queue, queue.empty()))
2249 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002250 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002251 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002252 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002253 self.log.error("All requests completed: %s" %
2254 (self.areAllNodeRequestsComplete(),))
2255 self.log.error("Merge client jobs: %s" %
2256 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002257 raise Exception("Timeout waiting for Zuul to settle")
2258 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002259
Paul Belanger174a8272017-03-14 13:20:10 -04002260 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002261 # have all build states propogated to zuul?
2262 if self.haveAllBuildsReported():
2263 # Join ensures that the queue is empty _and_ events have been
2264 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002265 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002266 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002267 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002268 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002269 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002270 self.areAllNodeRequestsComplete() and
2271 all(self.eventQueuesEmpty())):
2272 # The queue empty check is placed at the end to
2273 # ensure that if a component adds an event between
2274 # when locked the run handler and checked that the
2275 # components were stable, we don't erroneously
2276 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002277 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002278 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002279 self.log.debug("...settled.")
2280 return
2281 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002282 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002283 self.sched.wake_event.wait(0.1)
2284
2285 def countJobResults(self, jobs, result):
2286 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002287 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002288
James E. Blair96c6bf82016-01-15 16:20:40 -08002289 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002290 for job in self.history:
2291 if (job.name == name and
2292 (project is None or
2293 job.parameters['ZUUL_PROJECT'] == project)):
2294 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002295 raise Exception("Unable to find job %s in history" % name)
2296
2297 def assertEmptyQueues(self):
2298 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002299 for tenant in self.sched.abide.tenants.values():
2300 for pipeline in tenant.layout.pipelines.values():
2301 for queue in pipeline.queues:
2302 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002303 print('pipeline %s queue %s contents %s' % (
2304 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002305 self.assertEqual(len(queue.queue), 0,
2306 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002307
2308 def assertReportedStat(self, key, value=None, kind=None):
2309 start = time.time()
2310 while time.time() < (start + 5):
2311 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002312 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002313 if key == k:
2314 if value is None and kind is None:
2315 return
2316 elif value:
2317 if value == v:
2318 return
2319 elif kind:
2320 if v.endswith('|' + kind):
2321 return
2322 time.sleep(0.1)
2323
Clark Boylanb640e052014-04-03 16:41:46 -07002324 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002325
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002326 def assertBuilds(self, builds):
2327 """Assert that the running builds are as described.
2328
2329 The list of running builds is examined and must match exactly
2330 the list of builds described by the input.
2331
2332 :arg list builds: A list of dictionaries. Each item in the
2333 list must match the corresponding build in the build
2334 history, and each element of the dictionary must match the
2335 corresponding attribute of the build.
2336
2337 """
James E. Blair3158e282016-08-19 09:34:11 -07002338 try:
2339 self.assertEqual(len(self.builds), len(builds))
2340 for i, d in enumerate(builds):
2341 for k, v in d.items():
2342 self.assertEqual(
2343 getattr(self.builds[i], k), v,
2344 "Element %i in builds does not match" % (i,))
2345 except Exception:
2346 for build in self.builds:
2347 self.log.error("Running build: %s" % build)
2348 else:
2349 self.log.error("No running builds")
2350 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002351
James E. Blairb536ecc2016-08-31 10:11:42 -07002352 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002353 """Assert that the completed builds are as described.
2354
2355 The list of completed builds is examined and must match
2356 exactly the list of builds described by the input.
2357
2358 :arg list history: A list of dictionaries. Each item in the
2359 list must match the corresponding build in the build
2360 history, and each element of the dictionary must match the
2361 corresponding attribute of the build.
2362
James E. Blairb536ecc2016-08-31 10:11:42 -07002363 :arg bool ordered: If true, the history must match the order
2364 supplied, if false, the builds are permitted to have
2365 arrived in any order.
2366
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002367 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002368 def matches(history_item, item):
2369 for k, v in item.items():
2370 if getattr(history_item, k) != v:
2371 return False
2372 return True
James E. Blair3158e282016-08-19 09:34:11 -07002373 try:
2374 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002375 if ordered:
2376 for i, d in enumerate(history):
2377 if not matches(self.history[i], d):
2378 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002379 "Element %i in history does not match %s" %
2380 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002381 else:
2382 unseen = self.history[:]
2383 for i, d in enumerate(history):
2384 found = False
2385 for unseen_item in unseen:
2386 if matches(unseen_item, d):
2387 found = True
2388 unseen.remove(unseen_item)
2389 break
2390 if not found:
2391 raise Exception("No match found for element %i "
2392 "in history" % (i,))
2393 if unseen:
2394 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002395 except Exception:
2396 for build in self.history:
2397 self.log.error("Completed build: %s" % build)
2398 else:
2399 self.log.error("No completed builds")
2400 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002401
James E. Blair6ac368c2016-12-22 18:07:20 -08002402 def printHistory(self):
2403 """Log the build history.
2404
2405 This can be useful during tests to summarize what jobs have
2406 completed.
2407
2408 """
2409 self.log.debug("Build history:")
2410 for build in self.history:
2411 self.log.debug(build)
2412
James E. Blair59fdbac2015-12-07 17:08:06 -08002413 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002414 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2415
James E. Blair9ea70072017-04-19 16:05:30 -07002416 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002417 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002418 if not os.path.exists(root):
2419 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002420 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2421 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002422- tenant:
2423 name: openstack
2424 source:
2425 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002426 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002427 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002428 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002429 - org/project
2430 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002431 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002432 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002433 self.config.set('zuul', 'tenant_config',
2434 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002435 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002436
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002437 def addCommitToRepo(self, project, message, files,
2438 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002439 path = os.path.join(self.upstream_root, project)
2440 repo = git.Repo(path)
2441 repo.head.reference = branch
2442 zuul.merger.merger.reset_repo_to_head(repo)
2443 for fn, content in files.items():
2444 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002445 try:
2446 os.makedirs(os.path.dirname(fn))
2447 except OSError:
2448 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002449 with open(fn, 'w') as f:
2450 f.write(content)
2451 repo.index.add([fn])
2452 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002453 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002454 repo.heads[branch].commit = commit
2455 repo.head.reference = branch
2456 repo.git.clean('-x', '-f', '-d')
2457 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002458 if tag:
2459 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002460 return before
2461
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002462 def commitConfigUpdate(self, project_name, source_name):
2463 """Commit an update to zuul.yaml
2464
2465 This overwrites the zuul.yaml in the specificed project with
2466 the contents specified.
2467
2468 :arg str project_name: The name of the project containing
2469 zuul.yaml (e.g., common-config)
2470
2471 :arg str source_name: The path to the file (underneath the
2472 test fixture directory) whose contents should be used to
2473 replace zuul.yaml.
2474 """
2475
2476 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002477 files = {}
2478 with open(source_path, 'r') as f:
2479 data = f.read()
2480 layout = yaml.safe_load(data)
2481 files['zuul.yaml'] = data
2482 for item in layout:
2483 if 'job' in item:
2484 jobname = item['job']['name']
2485 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002486 before = self.addCommitToRepo(
2487 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002488 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002489 return before
2490
James E. Blair7fc8daa2016-08-08 15:37:15 -07002491 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002492
James E. Blair7fc8daa2016-08-08 15:37:15 -07002493 """Inject a Fake (Gerrit) event.
2494
2495 This method accepts a JSON-encoded event and simulates Zuul
2496 having received it from Gerrit. It could (and should)
2497 eventually apply to any connection type, but is currently only
2498 used with Gerrit connections. The name of the connection is
2499 used to look up the corresponding server, and the event is
2500 simulated as having been received by all Zuul connections
2501 attached to that server. So if two Gerrit connections in Zuul
2502 are connected to the same Gerrit server, and you invoke this
2503 method specifying the name of one of them, the event will be
2504 received by both.
2505
2506 .. note::
2507
2508 "self.fake_gerrit.addEvent" calls should be migrated to
2509 this method.
2510
2511 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002512 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002513 :arg str event: The JSON-encoded event.
2514
2515 """
2516 specified_conn = self.connections.connections[connection]
2517 for conn in self.connections.connections.values():
2518 if (isinstance(conn, specified_conn.__class__) and
2519 specified_conn.server == conn.server):
2520 conn.addEvent(event)
2521
James E. Blair3f876d52016-07-22 13:07:14 -07002522
2523class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002524 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002525 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002526
Joshua Heskethd78b4482015-09-14 16:56:34 -06002527
2528class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002529 def setup_config(self):
2530 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002531 for section_name in self.config.sections():
2532 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2533 section_name, re.I)
2534 if not con_match:
2535 continue
2536
2537 if self.config.get(section_name, 'driver') == 'sql':
2538 f = MySQLSchemaFixture()
2539 self.useFixture(f)
2540 if (self.config.get(section_name, 'dburi') ==
2541 '$MYSQL_FIXTURE_DBURI$'):
2542 self.config.set(section_name, 'dburi', f.dburi)