blob: a0a986a869968e38054bd8524336123e0da87ddf [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
James E. Blair1c236df2017-02-01 14:07:24 -080031from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070032import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
38import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060039import uuid
40
Clark Boylanb640e052014-04-03 16:41:46 -070041
42import git
43import gear
44import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080045import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080046import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060047import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070048import statsd
49import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blaire511d2f2016-12-08 15:22:26 -080055import zuul.driver.gerrit.gerritsource as gerritsource
56import zuul.driver.gerrit.gerritconnection as gerritconnection
Clark Boylanb640e052014-04-03 16:41:46 -070057import zuul.scheduler
58import zuul.webapp
59import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040060import zuul.executor.server
61import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080062import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070064import zuul.merger.merger
65import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070066import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080067import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070068
69FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
70 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080071
72KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070073
Clark Boylanb640e052014-04-03 16:41:46 -070074
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700106 categories = {'approved': ('Approved', -1, 1),
107 'code-review': ('Code-Review', -2, 2),
108 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700263 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
James E. Blairc2a5ed72017-02-20 14:12:01 -0500269 def getChangeMergedEvent(self):
270 event = {"submitter": {"name": "Jenkins",
271 "username": "jenkins"},
272 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
273 "patchSet": self.patchsets[-1],
274 "change": self.data,
275 "type": "change-merged",
276 "eventCreatedOn": 1487613810}
277 return event
278
James E. Blair8cce42e2016-10-18 08:18:36 -0700279 def getRefUpdatedEvent(self):
280 path = os.path.join(self.upstream_root, self.project)
281 repo = git.Repo(path)
282 oldrev = repo.heads[self.branch].commit.hexsha
283
284 event = {
285 "type": "ref-updated",
286 "submitter": {
287 "name": "User Name",
288 },
289 "refUpdate": {
290 "oldRev": oldrev,
291 "newRev": self.patchsets[-1]['revision'],
292 "refName": self.branch,
293 "project": self.project,
294 }
295 }
296 return event
297
Joshua Hesketh642824b2014-07-01 17:54:59 +1000298 def addApproval(self, category, value, username='reviewer_john',
299 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700300 if not granted_on:
301 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000302 approval = {
303 'description': self.categories[category][0],
304 'type': category,
305 'value': str(value),
306 'by': {
307 'username': username,
308 'email': username + '@example.com',
309 },
310 'grantedOn': int(granted_on)
311 }
Clark Boylanb640e052014-04-03 16:41:46 -0700312 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
313 if x['by']['username'] == username and x['type'] == category:
314 del self.patchsets[-1]['approvals'][i]
315 self.patchsets[-1]['approvals'].append(approval)
316 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000317 'author': {'email': 'author@example.com',
318 'name': 'Patchset Author',
319 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700320 'change': {'branch': self.branch,
321 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
322 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000323 'owner': {'email': 'owner@example.com',
324 'name': 'Change Owner',
325 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700326 'project': self.project,
327 'subject': self.subject,
328 'topic': 'master',
329 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700331 'patchSet': self.patchsets[-1],
332 'type': 'comment-added'}
333 self.data['submitRecords'] = self.getSubmitRecords()
334 return json.loads(json.dumps(event))
335
336 def getSubmitRecords(self):
337 status = {}
338 for cat in self.categories.keys():
339 status[cat] = 0
340
341 for a in self.patchsets[-1]['approvals']:
342 cur = status[a['type']]
343 cat_min, cat_max = self.categories[a['type']][1:]
344 new = int(a['value'])
345 if new == cat_min:
346 cur = new
347 elif abs(new) > abs(cur):
348 cur = new
349 status[a['type']] = cur
350
351 labels = []
352 ok = True
353 for typ, cat in self.categories.items():
354 cur = status[typ]
355 cat_min, cat_max = cat[1:]
356 if cur == cat_min:
357 value = 'REJECT'
358 ok = False
359 elif cur == cat_max:
360 value = 'OK'
361 else:
362 value = 'NEED'
363 ok = False
364 labels.append({'label': cat[0], 'status': value})
365 if ok:
366 return [{'status': 'OK'}]
367 return [{'status': 'NOT_READY',
368 'labels': labels}]
369
370 def setDependsOn(self, other, patchset):
371 self.depends_on_change = other
372 d = {'id': other.data['id'],
373 'number': other.data['number'],
374 'ref': other.patchsets[patchset - 1]['ref']
375 }
376 self.data['dependsOn'] = [d]
377
378 other.needed_by_changes.append(self)
379 needed = other.data.get('neededBy', [])
380 d = {'id': self.data['id'],
381 'number': self.data['number'],
382 'ref': self.patchsets[patchset - 1]['ref'],
383 'revision': self.patchsets[patchset - 1]['revision']
384 }
385 needed.append(d)
386 other.data['neededBy'] = needed
387
388 def query(self):
389 self.queried += 1
390 d = self.data.get('dependsOn')
391 if d:
392 d = d[0]
393 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
394 d['isCurrentPatchSet'] = True
395 else:
396 d['isCurrentPatchSet'] = False
397 return json.loads(json.dumps(self.data))
398
399 def setMerged(self):
400 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000401 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700402 return
403 if self.fail_merge:
404 return
405 self.data['status'] = 'MERGED'
406 self.open = False
407
408 path = os.path.join(self.upstream_root, self.project)
409 repo = git.Repo(path)
410 repo.heads[self.branch].commit = \
411 repo.commit(self.patchsets[-1]['revision'])
412
413 def setReported(self):
414 self.reported += 1
415
416
James E. Blaire511d2f2016-12-08 15:22:26 -0800417class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700418 """A Fake Gerrit connection for use in tests.
419
420 This subclasses
421 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
422 ability for tests to add changes to the fake Gerrit it represents.
423 """
424
Joshua Hesketh352264b2015-08-11 23:42:08 +1000425 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700426
James E. Blaire511d2f2016-12-08 15:22:26 -0800427 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700428 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800429 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000430 connection_config)
431
James E. Blair7fc8daa2016-08-08 15:37:15 -0700432 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700433 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
434 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000435 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700436 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200437 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700438
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700439 def addFakeChange(self, project, branch, subject, status='NEW',
440 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700441 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700442 self.change_number += 1
443 c = FakeChange(self, self.change_number, project, branch, subject,
444 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700445 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700446 self.changes[self.change_number] = c
447 return c
448
Clark Boylanb640e052014-04-03 16:41:46 -0700449 def review(self, project, changeid, message, action):
450 number, ps = changeid.split(',')
451 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000452
453 # Add the approval back onto the change (ie simulate what gerrit would
454 # do).
455 # Usually when zuul leaves a review it'll create a feedback loop where
456 # zuul's review enters another gerrit event (which is then picked up by
457 # zuul). However, we can't mimic this behaviour (by adding this
458 # approval event into the queue) as it stops jobs from checking what
459 # happens before this event is triggered. If a job needs to see what
460 # happens they can add their own verified event into the queue.
461 # Nevertheless, we can update change with the new review in gerrit.
462
James E. Blair8b5408c2016-08-08 15:37:46 -0700463 for cat in action.keys():
464 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000466
James E. Blair8b5408c2016-08-08 15:37:46 -0700467 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000468 if 'label' in action:
469 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000471
Clark Boylanb640e052014-04-03 16:41:46 -0700472 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000473
Clark Boylanb640e052014-04-03 16:41:46 -0700474 if 'submit' in action:
475 change.setMerged()
476 if message:
477 change.setReported()
478
479 def query(self, number):
480 change = self.changes.get(int(number))
481 if change:
482 return change.query()
483 return {}
484
James E. Blairc494d542014-08-06 09:23:52 -0700485 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700486 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700487 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800488 if query.startswith('change:'):
489 # Query a specific changeid
490 changeid = query[len('change:'):]
491 l = [change.query() for change in self.changes.values()
492 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700493 elif query.startswith('message:'):
494 # Query the content of a commit message
495 msg = query[len('message:'):].strip()
496 l = [change.query() for change in self.changes.values()
497 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800498 else:
499 # Query all open changes
500 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700501 return l
James E. Blairc494d542014-08-06 09:23:52 -0700502
Joshua Hesketh352264b2015-08-11 23:42:08 +1000503 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700504 pass
505
Joshua Hesketh352264b2015-08-11 23:42:08 +1000506 def getGitUrl(self, project):
507 return os.path.join(self.upstream_root, project.name)
508
Clark Boylanb640e052014-04-03 16:41:46 -0700509
510class BuildHistory(object):
511 def __init__(self, **kw):
512 self.__dict__.update(kw)
513
514 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700515 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
516 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700517
518
519class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200520 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700521 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700522 self.url = url
523
524 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700525 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700526 path = res.path
527 project = '/'.join(path.split('/')[2:-2])
528 ret = '001e# service=git-upload-pack\n'
529 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
530 'multi_ack thin-pack side-band side-band-64k ofs-delta '
531 'shallow no-progress include-tag multi_ack_detailed no-done\n')
532 path = os.path.join(self.upstream_root, project)
533 repo = git.Repo(path)
534 for ref in repo.refs:
535 r = ref.object.hexsha + ' ' + ref.path + '\n'
536 ret += '%04x%s' % (len(r) + 4, r)
537 ret += '0000'
538 return ret
539
540
Clark Boylanb640e052014-04-03 16:41:46 -0700541class FakeStatsd(threading.Thread):
542 def __init__(self):
543 threading.Thread.__init__(self)
544 self.daemon = True
545 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
546 self.sock.bind(('', 0))
547 self.port = self.sock.getsockname()[1]
548 self.wake_read, self.wake_write = os.pipe()
549 self.stats = []
550
551 def run(self):
552 while True:
553 poll = select.poll()
554 poll.register(self.sock, select.POLLIN)
555 poll.register(self.wake_read, select.POLLIN)
556 ret = poll.poll()
557 for (fd, event) in ret:
558 if fd == self.sock.fileno():
559 data = self.sock.recvfrom(1024)
560 if not data:
561 return
562 self.stats.append(data[0])
563 if fd == self.wake_read:
564 return
565
566 def stop(self):
567 os.write(self.wake_write, '1\n')
568
569
James E. Blaire1767bc2016-08-02 10:00:27 -0700570class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700571 log = logging.getLogger("zuul.test")
572
Paul Belanger174a8272017-03-14 13:20:10 -0400573 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700574 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400575 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700576 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700577 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700578 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700579 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700580 # TODOv3(jeblair): self.node is really "the image of the node
581 # assigned". We should rename it (self.node_image?) if we
582 # keep using it like this, or we may end up exposing more of
583 # the complexity around multi-node jobs here
584 # (self.nodes[0].image?)
585 self.node = None
586 if len(self.parameters.get('nodes')) == 1:
587 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700588 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100589 self.pipeline = self.parameters['ZUUL_PIPELINE']
590 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700591 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700592 self.wait_condition = threading.Condition()
593 self.waiting = False
594 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500595 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700596 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700597 self.changes = None
598 if 'ZUUL_CHANGE_IDS' in self.parameters:
599 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700600
James E. Blair3158e282016-08-19 09:34:11 -0700601 def __repr__(self):
602 waiting = ''
603 if self.waiting:
604 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100605 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
606 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700607
Clark Boylanb640e052014-04-03 16:41:46 -0700608 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700609 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700610 self.wait_condition.acquire()
611 self.wait_condition.notify()
612 self.waiting = False
613 self.log.debug("Build %s released" % self.unique)
614 self.wait_condition.release()
615
616 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700617 """Return whether this build is being held.
618
619 :returns: Whether the build is being held.
620 :rtype: bool
621 """
622
Clark Boylanb640e052014-04-03 16:41:46 -0700623 self.wait_condition.acquire()
624 if self.waiting:
625 ret = True
626 else:
627 ret = False
628 self.wait_condition.release()
629 return ret
630
631 def _wait(self):
632 self.wait_condition.acquire()
633 self.waiting = True
634 self.log.debug("Build %s waiting" % self.unique)
635 self.wait_condition.wait()
636 self.wait_condition.release()
637
638 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700639 self.log.debug('Running build %s' % self.unique)
640
Paul Belanger174a8272017-03-14 13:20:10 -0400641 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700642 self.log.debug('Holding build %s' % self.unique)
643 self._wait()
644 self.log.debug("Build %s continuing" % self.unique)
645
James E. Blair412fba82017-01-26 15:00:50 -0800646 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700647 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800648 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700649 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800650 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500651 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800652 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700653
James E. Blaire1767bc2016-08-02 10:00:27 -0700654 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700655
James E. Blaira5dba232016-08-08 15:53:24 -0700656 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400657 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700658 for change in changes:
659 if self.hasChanges(change):
660 return True
661 return False
662
James E. Blaire7b99a02016-08-05 14:27:34 -0700663 def hasChanges(self, *changes):
664 """Return whether this build has certain changes in its git repos.
665
666 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700667 are expected to be present (in order) in the git repository of
668 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700669
670 :returns: Whether the build has the indicated changes.
671 :rtype: bool
672
673 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800674 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500675 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800676 try:
677 repo = git.Repo(path)
678 except NoSuchPathError as e:
679 self.log.debug('%s' % e)
680 return False
681 ref = self.parameters['ZUUL_REF']
682 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
683 commit_message = '%s-1' % change.subject
684 self.log.debug("Checking if build %s has changes; commit_message "
685 "%s; repo_messages %s" % (self, commit_message,
686 repo_messages))
687 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700688 self.log.debug(" messages do not match")
689 return False
690 self.log.debug(" OK")
691 return True
692
Clark Boylanb640e052014-04-03 16:41:46 -0700693
Paul Belanger174a8272017-03-14 13:20:10 -0400694class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
695 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700696
Paul Belanger174a8272017-03-14 13:20:10 -0400697 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700698 they will report that they have started but then pause until
699 released before reporting completion. This attribute may be
700 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400701 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700702 be explicitly released.
703
704 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800705 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700706 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800707 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400708 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700709 self.hold_jobs_in_build = False
710 self.lock = threading.Lock()
711 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700712 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700713 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700714 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800715
James E. Blaira5dba232016-08-08 15:53:24 -0700716 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400717 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700718
719 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700720 :arg Change change: The :py:class:`~tests.base.FakeChange`
721 instance which should cause the job to fail. This job
722 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700723
724 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700725 l = self.fail_tests.get(name, [])
726 l.append(change)
727 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800728
James E. Blair962220f2016-08-03 11:22:38 -0700729 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700730 """Release a held build.
731
732 :arg str regex: A regular expression which, if supplied, will
733 cause only builds with matching names to be released. If
734 not supplied, all builds will be released.
735
736 """
James E. Blair962220f2016-08-03 11:22:38 -0700737 builds = self.running_builds[:]
738 self.log.debug("Releasing build %s (%s)" % (regex,
739 len(self.running_builds)))
740 for build in builds:
741 if not regex or re.match(regex, build.name):
742 self.log.debug("Releasing build %s" %
743 (build.parameters['ZUUL_UUID']))
744 build.release()
745 else:
746 self.log.debug("Not releasing build %s" %
747 (build.parameters['ZUUL_UUID']))
748 self.log.debug("Done releasing builds %s (%s)" %
749 (regex, len(self.running_builds)))
750
Paul Belanger174a8272017-03-14 13:20:10 -0400751 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700752 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700753 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700754 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700755 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800756 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500757 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800758 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100759 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
760 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700761
762 def stopJob(self, job):
763 self.log.debug("handle stop")
764 parameters = json.loads(job.arguments)
765 uuid = parameters['uuid']
766 for build in self.running_builds:
767 if build.unique == uuid:
768 build.aborted = True
769 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400770 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700771
Joshua Hesketh50c21782016-10-13 21:34:14 +1100772
Paul Belanger174a8272017-03-14 13:20:10 -0400773class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700774 def doMergeChanges(self, items):
775 # Get a merger in order to update the repos involved in this job.
776 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
777 if not commit: # merge conflict
778 self.recordResult('MERGER_FAILURE')
779 return commit
780
781 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400782 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400783 self.executor_server.lock.acquire()
784 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700785 BuildHistory(name=build.name, result=result, changes=build.changes,
786 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800787 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700788 pipeline=build.parameters['ZUUL_PIPELINE'])
789 )
Paul Belanger174a8272017-03-14 13:20:10 -0400790 self.executor_server.running_builds.remove(build)
791 del self.executor_server.job_builds[self.job.unique]
792 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700793
794 def runPlaybooks(self, args):
795 build = self.executor_server.job_builds[self.job.unique]
796 build.jobdir = self.jobdir
797
798 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
799 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800800 return result
801
Monty Taylore6562aa2017-02-20 07:37:39 -0500802 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400803 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800804
Paul Belanger174a8272017-03-14 13:20:10 -0400805 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600806 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500807 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800808 else:
809 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700810 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800811
James E. Blairad8dca02017-02-21 11:48:32 -0500812 def getHostList(self, args):
813 self.log.debug("hostlist")
814 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400815 for host in hosts:
816 host['host_vars']['ansible_connection'] = 'local'
817
818 hosts.append(dict(
819 name='localhost',
820 host_vars=dict(ansible_connection='local'),
821 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500822 return hosts
823
James E. Blairf5dbd002015-12-23 15:26:17 -0800824
Clark Boylanb640e052014-04-03 16:41:46 -0700825class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700826 """A Gearman server for use in tests.
827
828 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
829 added to the queue but will not be distributed to workers
830 until released. This attribute may be changed at any time and
831 will take effect for subsequently enqueued jobs, but
832 previously held jobs will still need to be explicitly
833 released.
834
835 """
836
Clark Boylanb640e052014-04-03 16:41:46 -0700837 def __init__(self):
838 self.hold_jobs_in_queue = False
839 super(FakeGearmanServer, self).__init__(0)
840
841 def getJobForConnection(self, connection, peek=False):
842 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
843 for job in queue:
844 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400845 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700846 job.waiting = self.hold_jobs_in_queue
847 else:
848 job.waiting = False
849 if job.waiting:
850 continue
851 if job.name in connection.functions:
852 if not peek:
853 queue.remove(job)
854 connection.related_jobs[job.handle] = job
855 job.worker_connection = connection
856 job.running = True
857 return job
858 return None
859
860 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700861 """Release a held job.
862
863 :arg str regex: A regular expression which, if supplied, will
864 cause only jobs with matching names to be released. If
865 not supplied, all jobs will be released.
866 """
Clark Boylanb640e052014-04-03 16:41:46 -0700867 released = False
868 qlen = (len(self.high_queue) + len(self.normal_queue) +
869 len(self.low_queue))
870 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
871 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400872 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700873 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500874 parameters = json.loads(job.arguments)
875 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700876 self.log.debug("releasing queued job %s" %
877 job.unique)
878 job.waiting = False
879 released = True
880 else:
881 self.log.debug("not releasing queued job %s" %
882 job.unique)
883 if released:
884 self.wakeConnections()
885 qlen = (len(self.high_queue) + len(self.normal_queue) +
886 len(self.low_queue))
887 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
888
889
890class FakeSMTP(object):
891 log = logging.getLogger('zuul.FakeSMTP')
892
893 def __init__(self, messages, server, port):
894 self.server = server
895 self.port = port
896 self.messages = messages
897
898 def sendmail(self, from_email, to_email, msg):
899 self.log.info("Sending email from %s, to %s, with msg %s" % (
900 from_email, to_email, msg))
901
902 headers = msg.split('\n\n', 1)[0]
903 body = msg.split('\n\n', 1)[1]
904
905 self.messages.append(dict(
906 from_email=from_email,
907 to_email=to_email,
908 msg=msg,
909 headers=headers,
910 body=body,
911 ))
912
913 return True
914
915 def quit(self):
916 return True
917
918
James E. Blairdce6cea2016-12-20 16:45:32 -0800919class FakeNodepool(object):
920 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800921 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800922
923 log = logging.getLogger("zuul.test.FakeNodepool")
924
925 def __init__(self, host, port, chroot):
926 self.client = kazoo.client.KazooClient(
927 hosts='%s:%s%s' % (host, port, chroot))
928 self.client.start()
929 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800930 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800931 self.thread = threading.Thread(target=self.run)
932 self.thread.daemon = True
933 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800934 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800935
936 def stop(self):
937 self._running = False
938 self.thread.join()
939 self.client.stop()
940 self.client.close()
941
942 def run(self):
943 while self._running:
944 self._run()
945 time.sleep(0.1)
946
947 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800948 if self.paused:
949 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800950 for req in self.getNodeRequests():
951 self.fulfillRequest(req)
952
953 def getNodeRequests(self):
954 try:
955 reqids = self.client.get_children(self.REQUEST_ROOT)
956 except kazoo.exceptions.NoNodeError:
957 return []
958 reqs = []
959 for oid in sorted(reqids):
960 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800961 try:
962 data, stat = self.client.get(path)
963 data = json.loads(data)
964 data['_oid'] = oid
965 reqs.append(data)
966 except kazoo.exceptions.NoNodeError:
967 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800968 return reqs
969
James E. Blaire18d4602017-01-05 11:17:28 -0800970 def getNodes(self):
971 try:
972 nodeids = self.client.get_children(self.NODE_ROOT)
973 except kazoo.exceptions.NoNodeError:
974 return []
975 nodes = []
976 for oid in sorted(nodeids):
977 path = self.NODE_ROOT + '/' + oid
978 data, stat = self.client.get(path)
979 data = json.loads(data)
980 data['_oid'] = oid
981 try:
982 lockfiles = self.client.get_children(path + '/lock')
983 except kazoo.exceptions.NoNodeError:
984 lockfiles = []
985 if lockfiles:
986 data['_lock'] = True
987 else:
988 data['_lock'] = False
989 nodes.append(data)
990 return nodes
991
James E. Blaira38c28e2017-01-04 10:33:20 -0800992 def makeNode(self, request_id, node_type):
993 now = time.time()
994 path = '/nodepool/nodes/'
995 data = dict(type=node_type,
996 provider='test-provider',
997 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -0400998 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -0500999 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001000 public_ipv4='127.0.0.1',
1001 private_ipv4=None,
1002 public_ipv6=None,
1003 allocated_to=request_id,
1004 state='ready',
1005 state_time=now,
1006 created_time=now,
1007 updated_time=now,
1008 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001009 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001010 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001011 data = json.dumps(data)
1012 path = self.client.create(path, data,
1013 makepath=True,
1014 sequence=True)
1015 nodeid = path.split("/")[-1]
1016 return nodeid
1017
James E. Blair6ab79e02017-01-06 10:10:17 -08001018 def addFailRequest(self, request):
1019 self.fail_requests.add(request['_oid'])
1020
James E. Blairdce6cea2016-12-20 16:45:32 -08001021 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001022 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001023 return
1024 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001025 oid = request['_oid']
1026 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001027
James E. Blair6ab79e02017-01-06 10:10:17 -08001028 if oid in self.fail_requests:
1029 request['state'] = 'failed'
1030 else:
1031 request['state'] = 'fulfilled'
1032 nodes = []
1033 for node in request['node_types']:
1034 nodeid = self.makeNode(oid, node)
1035 nodes.append(nodeid)
1036 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001037
James E. Blaira38c28e2017-01-04 10:33:20 -08001038 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001039 path = self.REQUEST_ROOT + '/' + oid
1040 data = json.dumps(request)
1041 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1042 self.client.set(path, data)
1043
1044
James E. Blair498059b2016-12-20 13:50:13 -08001045class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001046 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001047 super(ChrootedKazooFixture, self).__init__()
1048
1049 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1050 if ':' in zk_host:
1051 host, port = zk_host.split(':')
1052 else:
1053 host = zk_host
1054 port = None
1055
1056 self.zookeeper_host = host
1057
1058 if not port:
1059 self.zookeeper_port = 2181
1060 else:
1061 self.zookeeper_port = int(port)
1062
Clark Boylan621ec9a2017-04-07 17:41:33 -07001063 self.test_id = test_id
1064
James E. Blair498059b2016-12-20 13:50:13 -08001065 def _setUp(self):
1066 # Make sure the test chroot paths do not conflict
1067 random_bits = ''.join(random.choice(string.ascii_lowercase +
1068 string.ascii_uppercase)
1069 for x in range(8))
1070
Clark Boylan621ec9a2017-04-07 17:41:33 -07001071 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001072 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1073
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001074 self.addCleanup(self._cleanup)
1075
James E. Blair498059b2016-12-20 13:50:13 -08001076 # Ensure the chroot path exists and clean up any pre-existing znodes.
1077 _tmp_client = kazoo.client.KazooClient(
1078 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1079 _tmp_client.start()
1080
1081 if _tmp_client.exists(self.zookeeper_chroot):
1082 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1083
1084 _tmp_client.ensure_path(self.zookeeper_chroot)
1085 _tmp_client.stop()
1086 _tmp_client.close()
1087
James E. Blair498059b2016-12-20 13:50:13 -08001088 def _cleanup(self):
1089 '''Remove the chroot path.'''
1090 # Need a non-chroot'ed client to remove the chroot path
1091 _tmp_client = kazoo.client.KazooClient(
1092 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1093 _tmp_client.start()
1094 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1095 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001096 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001097
1098
Joshua Heskethd78b4482015-09-14 16:56:34 -06001099class MySQLSchemaFixture(fixtures.Fixture):
1100 def setUp(self):
1101 super(MySQLSchemaFixture, self).setUp()
1102
1103 random_bits = ''.join(random.choice(string.ascii_lowercase +
1104 string.ascii_uppercase)
1105 for x in range(8))
1106 self.name = '%s_%s' % (random_bits, os.getpid())
1107 self.passwd = uuid.uuid4().hex
1108 db = pymysql.connect(host="localhost",
1109 user="openstack_citest",
1110 passwd="openstack_citest",
1111 db="openstack_citest")
1112 cur = db.cursor()
1113 cur.execute("create database %s" % self.name)
1114 cur.execute(
1115 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1116 (self.name, self.name, self.passwd))
1117 cur.execute("flush privileges")
1118
1119 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1120 self.passwd,
1121 self.name)
1122 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1123 self.addCleanup(self.cleanup)
1124
1125 def cleanup(self):
1126 db = pymysql.connect(host="localhost",
1127 user="openstack_citest",
1128 passwd="openstack_citest",
1129 db="openstack_citest")
1130 cur = db.cursor()
1131 cur.execute("drop database %s" % self.name)
1132 cur.execute("drop user '%s'@'localhost'" % self.name)
1133 cur.execute("flush privileges")
1134
1135
Maru Newby3fe5f852015-01-13 04:22:14 +00001136class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001137 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001138 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001139
James E. Blair1c236df2017-02-01 14:07:24 -08001140 def attachLogs(self, *args):
1141 def reader():
1142 self._log_stream.seek(0)
1143 while True:
1144 x = self._log_stream.read(4096)
1145 if not x:
1146 break
1147 yield x.encode('utf8')
1148 content = testtools.content.content_from_reader(
1149 reader,
1150 testtools.content_type.UTF8_TEXT,
1151 False)
1152 self.addDetail('logging', content)
1153
Clark Boylanb640e052014-04-03 16:41:46 -07001154 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001155 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001156 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1157 try:
1158 test_timeout = int(test_timeout)
1159 except ValueError:
1160 # If timeout value is invalid do not set a timeout.
1161 test_timeout = 0
1162 if test_timeout > 0:
1163 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1164
1165 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1166 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1167 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1168 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1169 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1170 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1171 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1172 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1173 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1174 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001175 self._log_stream = StringIO()
1176 self.addOnException(self.attachLogs)
1177 else:
1178 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001179
James E. Blair1c236df2017-02-01 14:07:24 -08001180 handler = logging.StreamHandler(self._log_stream)
1181 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1182 '%(levelname)-8s %(message)s')
1183 handler.setFormatter(formatter)
1184
1185 logger = logging.getLogger()
1186 logger.setLevel(logging.DEBUG)
1187 logger.addHandler(handler)
1188
1189 # NOTE(notmorgan): Extract logging overrides for specific
1190 # libraries from the OS_LOG_DEFAULTS env and create loggers
1191 # for each. This is used to limit the output during test runs
1192 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001193 log_defaults_from_env = os.environ.get(
1194 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001195 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001196
James E. Blairdce6cea2016-12-20 16:45:32 -08001197 if log_defaults_from_env:
1198 for default in log_defaults_from_env.split(','):
1199 try:
1200 name, level_str = default.split('=', 1)
1201 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001202 logger = logging.getLogger(name)
1203 logger.setLevel(level)
1204 logger.addHandler(handler)
1205 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001206 except ValueError:
1207 # NOTE(notmorgan): Invalid format of the log default,
1208 # skip and don't try and apply a logger for the
1209 # specified module
1210 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001211
Maru Newby3fe5f852015-01-13 04:22:14 +00001212
1213class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001214 """A test case with a functioning Zuul.
1215
1216 The following class variables are used during test setup and can
1217 be overidden by subclasses but are effectively read-only once a
1218 test method starts running:
1219
1220 :cvar str config_file: This points to the main zuul config file
1221 within the fixtures directory. Subclasses may override this
1222 to obtain a different behavior.
1223
1224 :cvar str tenant_config_file: This is the tenant config file
1225 (which specifies from what git repos the configuration should
1226 be loaded). It defaults to the value specified in
1227 `config_file` but can be overidden by subclasses to obtain a
1228 different tenant/project layout while using the standard main
1229 configuration.
1230
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001231 :cvar bool create_project_keys: Indicates whether Zuul should
1232 auto-generate keys for each project, or whether the test
1233 infrastructure should insert dummy keys to save time during
1234 startup. Defaults to False.
1235
James E. Blaire7b99a02016-08-05 14:27:34 -07001236 The following are instance variables that are useful within test
1237 methods:
1238
1239 :ivar FakeGerritConnection fake_<connection>:
1240 A :py:class:`~tests.base.FakeGerritConnection` will be
1241 instantiated for each connection present in the config file
1242 and stored here. For instance, `fake_gerrit` will hold the
1243 FakeGerritConnection object for a connection named `gerrit`.
1244
1245 :ivar FakeGearmanServer gearman_server: An instance of
1246 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1247 server that all of the Zuul components in this test use to
1248 communicate with each other.
1249
Paul Belanger174a8272017-03-14 13:20:10 -04001250 :ivar RecordingExecutorServer executor_server: An instance of
1251 :py:class:`~tests.base.RecordingExecutorServer` which is the
1252 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001253
1254 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1255 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001256 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001257 list upon completion.
1258
1259 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1260 objects representing completed builds. They are appended to
1261 the list in the order they complete.
1262
1263 """
1264
James E. Blair83005782015-12-11 14:46:03 -08001265 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001266 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001267 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001268
1269 def _startMerger(self):
1270 self.merge_server = zuul.merger.server.MergeServer(self.config,
1271 self.connections)
1272 self.merge_server.start()
1273
Maru Newby3fe5f852015-01-13 04:22:14 +00001274 def setUp(self):
1275 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001276
1277 self.setupZK()
1278
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001279 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001280 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001281 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1282 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001283 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001284 tmp_root = tempfile.mkdtemp(
1285 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001286 self.test_root = os.path.join(tmp_root, "zuul-test")
1287 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001288 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001289 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001290 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001291
1292 if os.path.exists(self.test_root):
1293 shutil.rmtree(self.test_root)
1294 os.makedirs(self.test_root)
1295 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001296 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001297
1298 # Make per test copy of Configuration.
1299 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001300 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001301 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001302 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001303 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001304 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001305 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001306
1307 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001308 # TODOv3(jeblair): remove these and replace with new git
1309 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001310 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001311 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001312 self.init_repo("org/project5")
1313 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001314 self.init_repo("org/one-job-project")
1315 self.init_repo("org/nonvoting-project")
1316 self.init_repo("org/templated-project")
1317 self.init_repo("org/layered-project")
1318 self.init_repo("org/node-project")
1319 self.init_repo("org/conflict-project")
1320 self.init_repo("org/noop-project")
1321 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001322 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001323
1324 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001325 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1326 # see: https://github.com/jsocol/pystatsd/issues/61
1327 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001328 os.environ['STATSD_PORT'] = str(self.statsd.port)
1329 self.statsd.start()
1330 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001331 reload_module(statsd)
1332 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001333
1334 self.gearman_server = FakeGearmanServer()
1335
1336 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001337 self.log.info("Gearman server on port %s" %
1338 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001339
James E. Blaire511d2f2016-12-08 15:22:26 -08001340 gerritsource.GerritSource.replication_timeout = 1.5
1341 gerritsource.GerritSource.replication_retry_interval = 0.5
1342 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001343
Joshua Hesketh352264b2015-08-11 23:42:08 +10001344 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001345
Jan Hruban6b71aff2015-10-22 16:58:08 +02001346 self.event_queues = [
1347 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001348 self.sched.trigger_event_queue,
1349 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001350 ]
1351
James E. Blairfef78942016-03-11 16:28:56 -08001352 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001353 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001354
Clark Boylanb640e052014-04-03 16:41:46 -07001355 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001356 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001357 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001358 return FakeURLOpener(self.upstream_root, *args, **kw)
1359
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001360 old_urlopen = urllib.request.urlopen
1361 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001362
James E. Blair3f876d52016-07-22 13:07:14 -07001363 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001364
Paul Belanger174a8272017-03-14 13:20:10 -04001365 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001366 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001367 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001368 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001369 _test_root=self.test_root,
1370 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001371 self.executor_server.start()
1372 self.history = self.executor_server.build_history
1373 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001374
Paul Belanger174a8272017-03-14 13:20:10 -04001375 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001376 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001377 self.merge_client = zuul.merger.client.MergeClient(
1378 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001379 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001380 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001381 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001382
James E. Blair0d5a36e2017-02-21 10:53:44 -05001383 self.fake_nodepool = FakeNodepool(
1384 self.zk_chroot_fixture.zookeeper_host,
1385 self.zk_chroot_fixture.zookeeper_port,
1386 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001387
Paul Belanger174a8272017-03-14 13:20:10 -04001388 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001389 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001390 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001391 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001392
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001393 self.webapp = zuul.webapp.WebApp(
1394 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001395 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001396
1397 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001398 self.webapp.start()
1399 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001400 self.executor_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.addCleanup(self.shutdown)
1402
James E. Blairb9c0d772017-03-03 14:34:49 -08001403 self.sched.reconfigure(self.config)
1404 self.sched.resume()
1405
James E. Blaire18d4602017-01-05 11:17:28 -08001406 def tearDown(self):
1407 super(ZuulTestCase, self).tearDown()
1408 self.assertFinalState()
1409
James E. Blairfef78942016-03-11 16:28:56 -08001410 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001411 # Set up gerrit related fakes
1412 # Set a changes database so multiple FakeGerrit's can report back to
1413 # a virtual canonical database given by the configured hostname
1414 self.gerrit_changes_dbs = {}
1415
1416 def getGerritConnection(driver, name, config):
1417 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1418 con = FakeGerritConnection(driver, name, config,
1419 changes_db=db,
1420 upstream_root=self.upstream_root)
1421 self.event_queues.append(con.event_queue)
1422 setattr(self, 'fake_' + name, con)
1423 return con
1424
1425 self.useFixture(fixtures.MonkeyPatch(
1426 'zuul.driver.gerrit.GerritDriver.getConnection',
1427 getGerritConnection))
1428
1429 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001430 # TODO(jhesketh): This should come from lib.connections for better
1431 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001432 # Register connections from the config
1433 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001434
Joshua Hesketh352264b2015-08-11 23:42:08 +10001435 def FakeSMTPFactory(*args, **kw):
1436 args = [self.smtp_messages] + list(args)
1437 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001438
Joshua Hesketh352264b2015-08-11 23:42:08 +10001439 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001440
James E. Blaire511d2f2016-12-08 15:22:26 -08001441 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001442 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001443 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001444
James E. Blair83005782015-12-11 14:46:03 -08001445 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001446 # This creates the per-test configuration object. It can be
1447 # overriden by subclasses, but should not need to be since it
1448 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001449 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001450 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001451 if hasattr(self, 'tenant_config_file'):
1452 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001453 git_path = os.path.join(
1454 os.path.dirname(
1455 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1456 'git')
1457 if os.path.exists(git_path):
1458 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001459 project = reponame.replace('_', '/')
1460 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001461 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001462 self.setupAllProjectKeys()
1463
1464 def setupAllProjectKeys(self):
1465 if self.create_project_keys:
1466 return
1467
1468 path = self.config.get('zuul', 'tenant_config')
1469 with open(os.path.join(FIXTURE_DIR, path)) as f:
1470 tenant_config = yaml.safe_load(f.read())
1471 for tenant in tenant_config:
1472 sources = tenant['tenant']['source']
1473 for source, conf in sources.items():
1474 for project in conf.get('config-repos', []):
1475 self.setupProjectKeys(source, project)
1476 for project in conf.get('project-repos', []):
1477 self.setupProjectKeys(source, project)
1478
1479 def setupProjectKeys(self, source, project):
1480 # Make sure we set up an RSA key for the project so that we
1481 # don't spend time generating one:
1482
1483 key_root = os.path.join(self.state_root, 'keys')
1484 if not os.path.isdir(key_root):
1485 os.mkdir(key_root, 0o700)
1486 private_key_file = os.path.join(key_root, source, project + '.pem')
1487 private_key_dir = os.path.dirname(private_key_file)
1488 self.log.debug("Installing test keys for project %s at %s" % (
1489 project, private_key_file))
1490 if not os.path.isdir(private_key_dir):
1491 os.makedirs(private_key_dir)
1492 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1493 with open(private_key_file, 'w') as o:
1494 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001495
James E. Blair498059b2016-12-20 13:50:13 -08001496 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001497 self.zk_chroot_fixture = self.useFixture(
1498 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001499 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001500 self.zk_chroot_fixture.zookeeper_host,
1501 self.zk_chroot_fixture.zookeeper_port,
1502 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001503
James E. Blair96c6bf82016-01-15 16:20:40 -08001504 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001505 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001506
1507 files = {}
1508 for (dirpath, dirnames, filenames) in os.walk(source_path):
1509 for filename in filenames:
1510 test_tree_filepath = os.path.join(dirpath, filename)
1511 common_path = os.path.commonprefix([test_tree_filepath,
1512 source_path])
1513 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1514 with open(test_tree_filepath, 'r') as f:
1515 content = f.read()
1516 files[relative_filepath] = content
1517 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001518 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001519
James E. Blaire18d4602017-01-05 11:17:28 -08001520 def assertNodepoolState(self):
1521 # Make sure that there are no pending requests
1522
1523 requests = self.fake_nodepool.getNodeRequests()
1524 self.assertEqual(len(requests), 0)
1525
1526 nodes = self.fake_nodepool.getNodes()
1527 for node in nodes:
1528 self.assertFalse(node['_lock'], "Node %s is locked" %
1529 (node['_oid'],))
1530
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001531 def assertNoGeneratedKeys(self):
1532 # Make sure that Zuul did not generate any project keys
1533 # (unless it was supposed to).
1534
1535 if self.create_project_keys:
1536 return
1537
1538 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1539 test_key = i.read()
1540
1541 key_root = os.path.join(self.state_root, 'keys')
1542 for root, dirname, files in os.walk(key_root):
1543 for fn in files:
1544 with open(os.path.join(root, fn)) as f:
1545 self.assertEqual(test_key, f.read())
1546
Clark Boylanb640e052014-04-03 16:41:46 -07001547 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001548 # Make sure that git.Repo objects have been garbage collected.
1549 repos = []
1550 gc.collect()
1551 for obj in gc.get_objects():
1552 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001553 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001554 repos.append(obj)
1555 self.assertEqual(len(repos), 0)
1556 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001557 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001558 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001559 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001560 for tenant in self.sched.abide.tenants.values():
1561 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001562 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001563 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001564
1565 def shutdown(self):
1566 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001567 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001568 self.merge_server.stop()
1569 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001570 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001571 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001572 self.sched.stop()
1573 self.sched.join()
1574 self.statsd.stop()
1575 self.statsd.join()
1576 self.webapp.stop()
1577 self.webapp.join()
1578 self.rpc.stop()
1579 self.rpc.join()
1580 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001581 self.fake_nodepool.stop()
1582 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001583 threads = threading.enumerate()
1584 if len(threads) > 1:
1585 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001586 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001587
1588 def init_repo(self, project):
1589 parts = project.split('/')
1590 path = os.path.join(self.upstream_root, *parts[:-1])
1591 if not os.path.exists(path):
1592 os.makedirs(path)
1593 path = os.path.join(self.upstream_root, project)
1594 repo = git.Repo.init(path)
1595
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001596 with repo.config_writer() as config_writer:
1597 config_writer.set_value('user', 'email', 'user@example.com')
1598 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001599
Clark Boylanb640e052014-04-03 16:41:46 -07001600 repo.index.commit('initial commit')
1601 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001602
James E. Blair97d902e2014-08-21 13:25:56 -07001603 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001604 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001605 repo.git.clean('-x', '-f', '-d')
1606
James E. Blair97d902e2014-08-21 13:25:56 -07001607 def create_branch(self, project, branch):
1608 path = os.path.join(self.upstream_root, project)
1609 repo = git.Repo.init(path)
1610 fn = os.path.join(path, 'README')
1611
1612 branch_head = repo.create_head(branch)
1613 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001614 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001615 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001616 f.close()
1617 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001618 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001619
James E. Blair97d902e2014-08-21 13:25:56 -07001620 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001621 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001622 repo.git.clean('-x', '-f', '-d')
1623
Sachi King9f16d522016-03-16 12:20:45 +11001624 def create_commit(self, project):
1625 path = os.path.join(self.upstream_root, project)
1626 repo = git.Repo(path)
1627 repo.head.reference = repo.heads['master']
1628 file_name = os.path.join(path, 'README')
1629 with open(file_name, 'a') as f:
1630 f.write('creating fake commit\n')
1631 repo.index.add([file_name])
1632 commit = repo.index.commit('Creating a fake commit')
1633 return commit.hexsha
1634
James E. Blairb8c16472015-05-05 14:55:26 -07001635 def orderedRelease(self):
1636 # Run one build at a time to ensure non-race order:
1637 while len(self.builds):
1638 self.release(self.builds[0])
1639 self.waitUntilSettled()
1640
Clark Boylanb640e052014-04-03 16:41:46 -07001641 def release(self, job):
1642 if isinstance(job, FakeBuild):
1643 job.release()
1644 else:
1645 job.waiting = False
1646 self.log.debug("Queued job %s released" % job.unique)
1647 self.gearman_server.wakeConnections()
1648
1649 def getParameter(self, job, name):
1650 if isinstance(job, FakeBuild):
1651 return job.parameters[name]
1652 else:
1653 parameters = json.loads(job.arguments)
1654 return parameters[name]
1655
Clark Boylanb640e052014-04-03 16:41:46 -07001656 def haveAllBuildsReported(self):
1657 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001658 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001659 return False
1660 # Find out if every build that the worker has completed has been
1661 # reported back to Zuul. If it hasn't then that means a Gearman
1662 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001663 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001664 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001665 if not zbuild:
1666 # It has already been reported
1667 continue
1668 # It hasn't been reported yet.
1669 return False
1670 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001671 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001672 if connection.state == 'GRAB_WAIT':
1673 return False
1674 return True
1675
1676 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001677 builds = self.executor_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001678 for build in builds:
1679 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001680 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001681 for j in conn.related_jobs.values():
1682 if j.unique == build.uuid:
1683 client_job = j
1684 break
1685 if not client_job:
1686 self.log.debug("%s is not known to the gearman client" %
1687 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001688 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001689 if not client_job.handle:
1690 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001691 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001692 server_job = self.gearman_server.jobs.get(client_job.handle)
1693 if not server_job:
1694 self.log.debug("%s is not known to the gearman server" %
1695 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001696 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001697 if not hasattr(server_job, 'waiting'):
1698 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001699 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001700 if server_job.waiting:
1701 continue
James E. Blair17302972016-08-10 16:11:42 -07001702 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001703 self.log.debug("%s has not reported start" % build)
1704 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001705 worker_build = self.executor_server.job_builds.get(
1706 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001707 if worker_build:
1708 if worker_build.isWaiting():
1709 continue
1710 else:
1711 self.log.debug("%s is running" % worker_build)
1712 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001713 else:
James E. Blair962220f2016-08-03 11:22:38 -07001714 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001715 return False
1716 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001717
James E. Blairdce6cea2016-12-20 16:45:32 -08001718 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001719 if self.fake_nodepool.paused:
1720 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001721 if self.sched.nodepool.requests:
1722 return False
1723 return True
1724
Jan Hruban6b71aff2015-10-22 16:58:08 +02001725 def eventQueuesEmpty(self):
1726 for queue in self.event_queues:
1727 yield queue.empty()
1728
1729 def eventQueuesJoin(self):
1730 for queue in self.event_queues:
1731 queue.join()
1732
Clark Boylanb640e052014-04-03 16:41:46 -07001733 def waitUntilSettled(self):
1734 self.log.debug("Waiting until settled...")
1735 start = time.time()
1736 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001737 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001738 self.log.error("Timeout waiting for Zuul to settle")
1739 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001740 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001741 self.log.error(" %s: %s" % (queue, queue.empty()))
1742 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001743 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001744 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001745 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001746 self.log.error("All requests completed: %s" %
1747 (self.areAllNodeRequestsComplete(),))
1748 self.log.error("Merge client jobs: %s" %
1749 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001750 raise Exception("Timeout waiting for Zuul to settle")
1751 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001752
Paul Belanger174a8272017-03-14 13:20:10 -04001753 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001754 # have all build states propogated to zuul?
1755 if self.haveAllBuildsReported():
1756 # Join ensures that the queue is empty _and_ events have been
1757 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001758 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001759 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001760 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001761 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001762 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001763 self.areAllNodeRequestsComplete() and
1764 all(self.eventQueuesEmpty())):
1765 # The queue empty check is placed at the end to
1766 # ensure that if a component adds an event between
1767 # when locked the run handler and checked that the
1768 # components were stable, we don't erroneously
1769 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001770 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001771 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001772 self.log.debug("...settled.")
1773 return
1774 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001775 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001776 self.sched.wake_event.wait(0.1)
1777
1778 def countJobResults(self, jobs, result):
1779 jobs = filter(lambda x: x.result == result, jobs)
1780 return len(jobs)
1781
James E. Blair96c6bf82016-01-15 16:20:40 -08001782 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001783 for job in self.history:
1784 if (job.name == name and
1785 (project is None or
1786 job.parameters['ZUUL_PROJECT'] == project)):
1787 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001788 raise Exception("Unable to find job %s in history" % name)
1789
1790 def assertEmptyQueues(self):
1791 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001792 for tenant in self.sched.abide.tenants.values():
1793 for pipeline in tenant.layout.pipelines.values():
1794 for queue in pipeline.queues:
1795 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001796 print('pipeline %s queue %s contents %s' % (
1797 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001798 self.assertEqual(len(queue.queue), 0,
1799 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001800
1801 def assertReportedStat(self, key, value=None, kind=None):
1802 start = time.time()
1803 while time.time() < (start + 5):
1804 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001805 k, v = stat.split(':')
1806 if key == k:
1807 if value is None and kind is None:
1808 return
1809 elif value:
1810 if value == v:
1811 return
1812 elif kind:
1813 if v.endswith('|' + kind):
1814 return
1815 time.sleep(0.1)
1816
Clark Boylanb640e052014-04-03 16:41:46 -07001817 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001818
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001819 def assertBuilds(self, builds):
1820 """Assert that the running builds are as described.
1821
1822 The list of running builds is examined and must match exactly
1823 the list of builds described by the input.
1824
1825 :arg list builds: A list of dictionaries. Each item in the
1826 list must match the corresponding build in the build
1827 history, and each element of the dictionary must match the
1828 corresponding attribute of the build.
1829
1830 """
James E. Blair3158e282016-08-19 09:34:11 -07001831 try:
1832 self.assertEqual(len(self.builds), len(builds))
1833 for i, d in enumerate(builds):
1834 for k, v in d.items():
1835 self.assertEqual(
1836 getattr(self.builds[i], k), v,
1837 "Element %i in builds does not match" % (i,))
1838 except Exception:
1839 for build in self.builds:
1840 self.log.error("Running build: %s" % build)
1841 else:
1842 self.log.error("No running builds")
1843 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001844
James E. Blairb536ecc2016-08-31 10:11:42 -07001845 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001846 """Assert that the completed builds are as described.
1847
1848 The list of completed builds is examined and must match
1849 exactly the list of builds described by the input.
1850
1851 :arg list history: A list of dictionaries. Each item in the
1852 list must match the corresponding build in the build
1853 history, and each element of the dictionary must match the
1854 corresponding attribute of the build.
1855
James E. Blairb536ecc2016-08-31 10:11:42 -07001856 :arg bool ordered: If true, the history must match the order
1857 supplied, if false, the builds are permitted to have
1858 arrived in any order.
1859
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001860 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001861 def matches(history_item, item):
1862 for k, v in item.items():
1863 if getattr(history_item, k) != v:
1864 return False
1865 return True
James E. Blair3158e282016-08-19 09:34:11 -07001866 try:
1867 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001868 if ordered:
1869 for i, d in enumerate(history):
1870 if not matches(self.history[i], d):
1871 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001872 "Element %i in history does not match %s" %
1873 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001874 else:
1875 unseen = self.history[:]
1876 for i, d in enumerate(history):
1877 found = False
1878 for unseen_item in unseen:
1879 if matches(unseen_item, d):
1880 found = True
1881 unseen.remove(unseen_item)
1882 break
1883 if not found:
1884 raise Exception("No match found for element %i "
1885 "in history" % (i,))
1886 if unseen:
1887 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001888 except Exception:
1889 for build in self.history:
1890 self.log.error("Completed build: %s" % build)
1891 else:
1892 self.log.error("No completed builds")
1893 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001894
James E. Blair6ac368c2016-12-22 18:07:20 -08001895 def printHistory(self):
1896 """Log the build history.
1897
1898 This can be useful during tests to summarize what jobs have
1899 completed.
1900
1901 """
1902 self.log.debug("Build history:")
1903 for build in self.history:
1904 self.log.debug(build)
1905
James E. Blair59fdbac2015-12-07 17:08:06 -08001906 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001907 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1908
1909 def updateConfigLayout(self, path):
1910 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001911 if not os.path.exists(root):
1912 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001913 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1914 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001915- tenant:
1916 name: openstack
1917 source:
1918 gerrit:
1919 config-repos:
1920 - %s
1921 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001922 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001923 self.config.set('zuul', 'tenant_config',
1924 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001925 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08001926
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001927 def addCommitToRepo(self, project, message, files,
1928 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001929 path = os.path.join(self.upstream_root, project)
1930 repo = git.Repo(path)
1931 repo.head.reference = branch
1932 zuul.merger.merger.reset_repo_to_head(repo)
1933 for fn, content in files.items():
1934 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001935 try:
1936 os.makedirs(os.path.dirname(fn))
1937 except OSError:
1938 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001939 with open(fn, 'w') as f:
1940 f.write(content)
1941 repo.index.add([fn])
1942 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001943 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001944 repo.heads[branch].commit = commit
1945 repo.head.reference = branch
1946 repo.git.clean('-x', '-f', '-d')
1947 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001948 if tag:
1949 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001950 return before
1951
1952 def commitLayoutUpdate(self, orig_name, source_name):
1953 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07001954 source_name)
1955 to_copy = ['zuul.yaml']
1956 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
1957 to_copy.append('playbooks/{}'.format(playbook))
1958 commit_data = {}
1959 for source_file in to_copy:
1960 source_file_path = os.path.join(source_path, source_file)
1961 with open(source_file_path, 'r') as nt:
1962 commit_data[source_file] = nt.read()
1963 before = self.addCommitToRepo(
1964 orig_name, 'Pulling content from %s' % source_name,
1965 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08001966 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001967
James E. Blair7fc8daa2016-08-08 15:37:15 -07001968 def addEvent(self, connection, event):
1969 """Inject a Fake (Gerrit) event.
1970
1971 This method accepts a JSON-encoded event and simulates Zuul
1972 having received it from Gerrit. It could (and should)
1973 eventually apply to any connection type, but is currently only
1974 used with Gerrit connections. The name of the connection is
1975 used to look up the corresponding server, and the event is
1976 simulated as having been received by all Zuul connections
1977 attached to that server. So if two Gerrit connections in Zuul
1978 are connected to the same Gerrit server, and you invoke this
1979 method specifying the name of one of them, the event will be
1980 received by both.
1981
1982 .. note::
1983
1984 "self.fake_gerrit.addEvent" calls should be migrated to
1985 this method.
1986
1987 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07001988 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07001989 :arg str event: The JSON-encoded event.
1990
1991 """
1992 specified_conn = self.connections.connections[connection]
1993 for conn in self.connections.connections.values():
1994 if (isinstance(conn, specified_conn.__class__) and
1995 specified_conn.server == conn.server):
1996 conn.addEvent(event)
1997
James E. Blair3f876d52016-07-22 13:07:14 -07001998
1999class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002000 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002001 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002002
Joshua Heskethd78b4482015-09-14 16:56:34 -06002003
2004class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002005 def setup_config(self):
2006 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002007 for section_name in self.config.sections():
2008 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2009 section_name, re.I)
2010 if not con_match:
2011 continue
2012
2013 if self.config.get(section_name, 'driver') == 'sql':
2014 f = MySQLSchemaFixture()
2015 self.useFixture(f)
2016 if (self.config.get(section_name, 'dburi') ==
2017 '$MYSQL_FIXTURE_DBURI$'):
2018 self.config.set(section_name, 'dburi', f.dburi)