blob: 3d2f4c1c2c8bac700763db9d3ebc4fc06a1ba71d [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
James E. Blaira002b032017-04-18 10:35:48 -0700772 def stop(self):
773 for build in self.running_builds:
774 build.release()
775 super(RecordingExecutorServer, self).stop()
776
Joshua Hesketh50c21782016-10-13 21:34:14 +1100777
Paul Belanger174a8272017-03-14 13:20:10 -0400778class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700779 def doMergeChanges(self, items):
780 # Get a merger in order to update the repos involved in this job.
781 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
782 if not commit: # merge conflict
783 self.recordResult('MERGER_FAILURE')
784 return commit
785
786 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400787 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400788 self.executor_server.lock.acquire()
789 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700790 BuildHistory(name=build.name, result=result, changes=build.changes,
791 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800792 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700793 pipeline=build.parameters['ZUUL_PIPELINE'])
794 )
Paul Belanger174a8272017-03-14 13:20:10 -0400795 self.executor_server.running_builds.remove(build)
796 del self.executor_server.job_builds[self.job.unique]
797 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700798
799 def runPlaybooks(self, args):
800 build = self.executor_server.job_builds[self.job.unique]
801 build.jobdir = self.jobdir
802
803 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
804 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800805 return result
806
Monty Taylore6562aa2017-02-20 07:37:39 -0500807 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400808 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800809
Paul Belanger174a8272017-03-14 13:20:10 -0400810 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600811 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500812 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800813 else:
814 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700815 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800816
James E. Blairad8dca02017-02-21 11:48:32 -0500817 def getHostList(self, args):
818 self.log.debug("hostlist")
819 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400820 for host in hosts:
821 host['host_vars']['ansible_connection'] = 'local'
822
823 hosts.append(dict(
824 name='localhost',
825 host_vars=dict(ansible_connection='local'),
826 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500827 return hosts
828
James E. Blairf5dbd002015-12-23 15:26:17 -0800829
Clark Boylanb640e052014-04-03 16:41:46 -0700830class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700831 """A Gearman server for use in tests.
832
833 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
834 added to the queue but will not be distributed to workers
835 until released. This attribute may be changed at any time and
836 will take effect for subsequently enqueued jobs, but
837 previously held jobs will still need to be explicitly
838 released.
839
840 """
841
Clark Boylanb640e052014-04-03 16:41:46 -0700842 def __init__(self):
843 self.hold_jobs_in_queue = False
844 super(FakeGearmanServer, self).__init__(0)
845
846 def getJobForConnection(self, connection, peek=False):
847 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
848 for job in queue:
849 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400850 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700851 job.waiting = self.hold_jobs_in_queue
852 else:
853 job.waiting = False
854 if job.waiting:
855 continue
856 if job.name in connection.functions:
857 if not peek:
858 queue.remove(job)
859 connection.related_jobs[job.handle] = job
860 job.worker_connection = connection
861 job.running = True
862 return job
863 return None
864
865 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700866 """Release a held job.
867
868 :arg str regex: A regular expression which, if supplied, will
869 cause only jobs with matching names to be released. If
870 not supplied, all jobs will be released.
871 """
Clark Boylanb640e052014-04-03 16:41:46 -0700872 released = False
873 qlen = (len(self.high_queue) + len(self.normal_queue) +
874 len(self.low_queue))
875 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
876 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400877 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700878 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500879 parameters = json.loads(job.arguments)
880 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700881 self.log.debug("releasing queued job %s" %
882 job.unique)
883 job.waiting = False
884 released = True
885 else:
886 self.log.debug("not releasing queued job %s" %
887 job.unique)
888 if released:
889 self.wakeConnections()
890 qlen = (len(self.high_queue) + len(self.normal_queue) +
891 len(self.low_queue))
892 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
893
894
895class FakeSMTP(object):
896 log = logging.getLogger('zuul.FakeSMTP')
897
898 def __init__(self, messages, server, port):
899 self.server = server
900 self.port = port
901 self.messages = messages
902
903 def sendmail(self, from_email, to_email, msg):
904 self.log.info("Sending email from %s, to %s, with msg %s" % (
905 from_email, to_email, msg))
906
907 headers = msg.split('\n\n', 1)[0]
908 body = msg.split('\n\n', 1)[1]
909
910 self.messages.append(dict(
911 from_email=from_email,
912 to_email=to_email,
913 msg=msg,
914 headers=headers,
915 body=body,
916 ))
917
918 return True
919
920 def quit(self):
921 return True
922
923
James E. Blairdce6cea2016-12-20 16:45:32 -0800924class FakeNodepool(object):
925 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800926 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800927
928 log = logging.getLogger("zuul.test.FakeNodepool")
929
930 def __init__(self, host, port, chroot):
931 self.client = kazoo.client.KazooClient(
932 hosts='%s:%s%s' % (host, port, chroot))
933 self.client.start()
934 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800935 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800936 self.thread = threading.Thread(target=self.run)
937 self.thread.daemon = True
938 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800939 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800940
941 def stop(self):
942 self._running = False
943 self.thread.join()
944 self.client.stop()
945 self.client.close()
946
947 def run(self):
948 while self._running:
949 self._run()
950 time.sleep(0.1)
951
952 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800953 if self.paused:
954 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800955 for req in self.getNodeRequests():
956 self.fulfillRequest(req)
957
958 def getNodeRequests(self):
959 try:
960 reqids = self.client.get_children(self.REQUEST_ROOT)
961 except kazoo.exceptions.NoNodeError:
962 return []
963 reqs = []
964 for oid in sorted(reqids):
965 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800966 try:
967 data, stat = self.client.get(path)
968 data = json.loads(data)
969 data['_oid'] = oid
970 reqs.append(data)
971 except kazoo.exceptions.NoNodeError:
972 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800973 return reqs
974
James E. Blaire18d4602017-01-05 11:17:28 -0800975 def getNodes(self):
976 try:
977 nodeids = self.client.get_children(self.NODE_ROOT)
978 except kazoo.exceptions.NoNodeError:
979 return []
980 nodes = []
981 for oid in sorted(nodeids):
982 path = self.NODE_ROOT + '/' + oid
983 data, stat = self.client.get(path)
984 data = json.loads(data)
985 data['_oid'] = oid
986 try:
987 lockfiles = self.client.get_children(path + '/lock')
988 except kazoo.exceptions.NoNodeError:
989 lockfiles = []
990 if lockfiles:
991 data['_lock'] = True
992 else:
993 data['_lock'] = False
994 nodes.append(data)
995 return nodes
996
James E. Blaira38c28e2017-01-04 10:33:20 -0800997 def makeNode(self, request_id, node_type):
998 now = time.time()
999 path = '/nodepool/nodes/'
1000 data = dict(type=node_type,
1001 provider='test-provider',
1002 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001003 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001004 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001005 public_ipv4='127.0.0.1',
1006 private_ipv4=None,
1007 public_ipv6=None,
1008 allocated_to=request_id,
1009 state='ready',
1010 state_time=now,
1011 created_time=now,
1012 updated_time=now,
1013 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001014 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001015 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001016 data = json.dumps(data)
1017 path = self.client.create(path, data,
1018 makepath=True,
1019 sequence=True)
1020 nodeid = path.split("/")[-1]
1021 return nodeid
1022
James E. Blair6ab79e02017-01-06 10:10:17 -08001023 def addFailRequest(self, request):
1024 self.fail_requests.add(request['_oid'])
1025
James E. Blairdce6cea2016-12-20 16:45:32 -08001026 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001027 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001028 return
1029 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001030 oid = request['_oid']
1031 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001032
James E. Blair6ab79e02017-01-06 10:10:17 -08001033 if oid in self.fail_requests:
1034 request['state'] = 'failed'
1035 else:
1036 request['state'] = 'fulfilled'
1037 nodes = []
1038 for node in request['node_types']:
1039 nodeid = self.makeNode(oid, node)
1040 nodes.append(nodeid)
1041 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001042
James E. Blaira38c28e2017-01-04 10:33:20 -08001043 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001044 path = self.REQUEST_ROOT + '/' + oid
1045 data = json.dumps(request)
1046 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1047 self.client.set(path, data)
1048
1049
James E. Blair498059b2016-12-20 13:50:13 -08001050class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001051 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001052 super(ChrootedKazooFixture, self).__init__()
1053
1054 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1055 if ':' in zk_host:
1056 host, port = zk_host.split(':')
1057 else:
1058 host = zk_host
1059 port = None
1060
1061 self.zookeeper_host = host
1062
1063 if not port:
1064 self.zookeeper_port = 2181
1065 else:
1066 self.zookeeper_port = int(port)
1067
Clark Boylan621ec9a2017-04-07 17:41:33 -07001068 self.test_id = test_id
1069
James E. Blair498059b2016-12-20 13:50:13 -08001070 def _setUp(self):
1071 # Make sure the test chroot paths do not conflict
1072 random_bits = ''.join(random.choice(string.ascii_lowercase +
1073 string.ascii_uppercase)
1074 for x in range(8))
1075
Clark Boylan621ec9a2017-04-07 17:41:33 -07001076 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001077 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1078
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001079 self.addCleanup(self._cleanup)
1080
James E. Blair498059b2016-12-20 13:50:13 -08001081 # Ensure the chroot path exists and clean up any pre-existing znodes.
1082 _tmp_client = kazoo.client.KazooClient(
1083 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1084 _tmp_client.start()
1085
1086 if _tmp_client.exists(self.zookeeper_chroot):
1087 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1088
1089 _tmp_client.ensure_path(self.zookeeper_chroot)
1090 _tmp_client.stop()
1091 _tmp_client.close()
1092
James E. Blair498059b2016-12-20 13:50:13 -08001093 def _cleanup(self):
1094 '''Remove the chroot path.'''
1095 # Need a non-chroot'ed client to remove the chroot path
1096 _tmp_client = kazoo.client.KazooClient(
1097 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1098 _tmp_client.start()
1099 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1100 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001101 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001102
1103
Joshua Heskethd78b4482015-09-14 16:56:34 -06001104class MySQLSchemaFixture(fixtures.Fixture):
1105 def setUp(self):
1106 super(MySQLSchemaFixture, self).setUp()
1107
1108 random_bits = ''.join(random.choice(string.ascii_lowercase +
1109 string.ascii_uppercase)
1110 for x in range(8))
1111 self.name = '%s_%s' % (random_bits, os.getpid())
1112 self.passwd = uuid.uuid4().hex
1113 db = pymysql.connect(host="localhost",
1114 user="openstack_citest",
1115 passwd="openstack_citest",
1116 db="openstack_citest")
1117 cur = db.cursor()
1118 cur.execute("create database %s" % self.name)
1119 cur.execute(
1120 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1121 (self.name, self.name, self.passwd))
1122 cur.execute("flush privileges")
1123
1124 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1125 self.passwd,
1126 self.name)
1127 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1128 self.addCleanup(self.cleanup)
1129
1130 def cleanup(self):
1131 db = pymysql.connect(host="localhost",
1132 user="openstack_citest",
1133 passwd="openstack_citest",
1134 db="openstack_citest")
1135 cur = db.cursor()
1136 cur.execute("drop database %s" % self.name)
1137 cur.execute("drop user '%s'@'localhost'" % self.name)
1138 cur.execute("flush privileges")
1139
1140
Maru Newby3fe5f852015-01-13 04:22:14 +00001141class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001142 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001143 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001144
James E. Blair1c236df2017-02-01 14:07:24 -08001145 def attachLogs(self, *args):
1146 def reader():
1147 self._log_stream.seek(0)
1148 while True:
1149 x = self._log_stream.read(4096)
1150 if not x:
1151 break
1152 yield x.encode('utf8')
1153 content = testtools.content.content_from_reader(
1154 reader,
1155 testtools.content_type.UTF8_TEXT,
1156 False)
1157 self.addDetail('logging', content)
1158
Clark Boylanb640e052014-04-03 16:41:46 -07001159 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001160 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001161 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1162 try:
1163 test_timeout = int(test_timeout)
1164 except ValueError:
1165 # If timeout value is invalid do not set a timeout.
1166 test_timeout = 0
1167 if test_timeout > 0:
1168 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1169
1170 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1171 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1172 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1173 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1174 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1175 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1176 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1177 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1178 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1179 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001180 self._log_stream = StringIO()
1181 self.addOnException(self.attachLogs)
1182 else:
1183 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001184
James E. Blair1c236df2017-02-01 14:07:24 -08001185 handler = logging.StreamHandler(self._log_stream)
1186 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1187 '%(levelname)-8s %(message)s')
1188 handler.setFormatter(formatter)
1189
1190 logger = logging.getLogger()
1191 logger.setLevel(logging.DEBUG)
1192 logger.addHandler(handler)
1193
1194 # NOTE(notmorgan): Extract logging overrides for specific
1195 # libraries from the OS_LOG_DEFAULTS env and create loggers
1196 # for each. This is used to limit the output during test runs
1197 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001198 log_defaults_from_env = os.environ.get(
1199 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001200 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001201
James E. Blairdce6cea2016-12-20 16:45:32 -08001202 if log_defaults_from_env:
1203 for default in log_defaults_from_env.split(','):
1204 try:
1205 name, level_str = default.split('=', 1)
1206 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001207 logger = logging.getLogger(name)
1208 logger.setLevel(level)
1209 logger.addHandler(handler)
1210 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001211 except ValueError:
1212 # NOTE(notmorgan): Invalid format of the log default,
1213 # skip and don't try and apply a logger for the
1214 # specified module
1215 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001216
Maru Newby3fe5f852015-01-13 04:22:14 +00001217
1218class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001219 """A test case with a functioning Zuul.
1220
1221 The following class variables are used during test setup and can
1222 be overidden by subclasses but are effectively read-only once a
1223 test method starts running:
1224
1225 :cvar str config_file: This points to the main zuul config file
1226 within the fixtures directory. Subclasses may override this
1227 to obtain a different behavior.
1228
1229 :cvar str tenant_config_file: This is the tenant config file
1230 (which specifies from what git repos the configuration should
1231 be loaded). It defaults to the value specified in
1232 `config_file` but can be overidden by subclasses to obtain a
1233 different tenant/project layout while using the standard main
1234 configuration.
1235
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001236 :cvar bool create_project_keys: Indicates whether Zuul should
1237 auto-generate keys for each project, or whether the test
1238 infrastructure should insert dummy keys to save time during
1239 startup. Defaults to False.
1240
James E. Blaire7b99a02016-08-05 14:27:34 -07001241 The following are instance variables that are useful within test
1242 methods:
1243
1244 :ivar FakeGerritConnection fake_<connection>:
1245 A :py:class:`~tests.base.FakeGerritConnection` will be
1246 instantiated for each connection present in the config file
1247 and stored here. For instance, `fake_gerrit` will hold the
1248 FakeGerritConnection object for a connection named `gerrit`.
1249
1250 :ivar FakeGearmanServer gearman_server: An instance of
1251 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1252 server that all of the Zuul components in this test use to
1253 communicate with each other.
1254
Paul Belanger174a8272017-03-14 13:20:10 -04001255 :ivar RecordingExecutorServer executor_server: An instance of
1256 :py:class:`~tests.base.RecordingExecutorServer` which is the
1257 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001258
1259 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1260 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001261 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001262 list upon completion.
1263
1264 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1265 objects representing completed builds. They are appended to
1266 the list in the order they complete.
1267
1268 """
1269
James E. Blair83005782015-12-11 14:46:03 -08001270 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001271 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001272 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001273
1274 def _startMerger(self):
1275 self.merge_server = zuul.merger.server.MergeServer(self.config,
1276 self.connections)
1277 self.merge_server.start()
1278
Maru Newby3fe5f852015-01-13 04:22:14 +00001279 def setUp(self):
1280 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001281
1282 self.setupZK()
1283
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001284 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001285 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001286 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1287 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001288 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001289 tmp_root = tempfile.mkdtemp(
1290 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001291 self.test_root = os.path.join(tmp_root, "zuul-test")
1292 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001293 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001294 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001295 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001296
1297 if os.path.exists(self.test_root):
1298 shutil.rmtree(self.test_root)
1299 os.makedirs(self.test_root)
1300 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001301 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001302
1303 # Make per test copy of Configuration.
1304 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001305 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001306 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001307 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001308 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001309 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001310 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001311
1312 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001313 # TODOv3(jeblair): remove these and replace with new git
1314 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001315 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001316 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001317 self.init_repo("org/project5")
1318 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001319 self.init_repo("org/one-job-project")
1320 self.init_repo("org/nonvoting-project")
1321 self.init_repo("org/templated-project")
1322 self.init_repo("org/layered-project")
1323 self.init_repo("org/node-project")
1324 self.init_repo("org/conflict-project")
1325 self.init_repo("org/noop-project")
1326 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001327 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001328
1329 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001330 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1331 # see: https://github.com/jsocol/pystatsd/issues/61
1332 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001333 os.environ['STATSD_PORT'] = str(self.statsd.port)
1334 self.statsd.start()
1335 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001336 reload_module(statsd)
1337 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001338
1339 self.gearman_server = FakeGearmanServer()
1340
1341 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001342 self.log.info("Gearman server on port %s" %
1343 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001344
James E. Blaire511d2f2016-12-08 15:22:26 -08001345 gerritsource.GerritSource.replication_timeout = 1.5
1346 gerritsource.GerritSource.replication_retry_interval = 0.5
1347 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001348
Joshua Hesketh352264b2015-08-11 23:42:08 +10001349 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001350
Jan Hruban6b71aff2015-10-22 16:58:08 +02001351 self.event_queues = [
1352 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001353 self.sched.trigger_event_queue,
1354 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001355 ]
1356
James E. Blairfef78942016-03-11 16:28:56 -08001357 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001358 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001359
Clark Boylanb640e052014-04-03 16:41:46 -07001360 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001361 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001362 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001363 return FakeURLOpener(self.upstream_root, *args, **kw)
1364
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001365 old_urlopen = urllib.request.urlopen
1366 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001367
James E. Blair3f876d52016-07-22 13:07:14 -07001368 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001369
Paul Belanger174a8272017-03-14 13:20:10 -04001370 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001371 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001372 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001373 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001374 _test_root=self.test_root,
1375 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001376 self.executor_server.start()
1377 self.history = self.executor_server.build_history
1378 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001379
Paul Belanger174a8272017-03-14 13:20:10 -04001380 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001381 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001382 self.merge_client = zuul.merger.client.MergeClient(
1383 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001384 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001385 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001386 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001387
James E. Blair0d5a36e2017-02-21 10:53:44 -05001388 self.fake_nodepool = FakeNodepool(
1389 self.zk_chroot_fixture.zookeeper_host,
1390 self.zk_chroot_fixture.zookeeper_port,
1391 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001392
Paul Belanger174a8272017-03-14 13:20:10 -04001393 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001394 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001395 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001396 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001397
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001398 self.webapp = zuul.webapp.WebApp(
1399 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001400 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001401
1402 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001403 self.webapp.start()
1404 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001405 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001406 # Cleanups are run in reverse order
1407 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001408 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001409 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001410
James E. Blairb9c0d772017-03-03 14:34:49 -08001411 self.sched.reconfigure(self.config)
1412 self.sched.resume()
1413
James E. Blairfef78942016-03-11 16:28:56 -08001414 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001415 # Set up gerrit related fakes
1416 # Set a changes database so multiple FakeGerrit's can report back to
1417 # a virtual canonical database given by the configured hostname
1418 self.gerrit_changes_dbs = {}
1419
1420 def getGerritConnection(driver, name, config):
1421 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1422 con = FakeGerritConnection(driver, name, config,
1423 changes_db=db,
1424 upstream_root=self.upstream_root)
1425 self.event_queues.append(con.event_queue)
1426 setattr(self, 'fake_' + name, con)
1427 return con
1428
1429 self.useFixture(fixtures.MonkeyPatch(
1430 'zuul.driver.gerrit.GerritDriver.getConnection',
1431 getGerritConnection))
1432
1433 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001434 # TODO(jhesketh): This should come from lib.connections for better
1435 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001436 # Register connections from the config
1437 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001438
Joshua Hesketh352264b2015-08-11 23:42:08 +10001439 def FakeSMTPFactory(*args, **kw):
1440 args = [self.smtp_messages] + list(args)
1441 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001442
Joshua Hesketh352264b2015-08-11 23:42:08 +10001443 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001444
James E. Blaire511d2f2016-12-08 15:22:26 -08001445 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001446 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001447 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001448
James E. Blair83005782015-12-11 14:46:03 -08001449 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001450 # This creates the per-test configuration object. It can be
1451 # overriden by subclasses, but should not need to be since it
1452 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001453 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001454 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001455 if hasattr(self, 'tenant_config_file'):
1456 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001457 git_path = os.path.join(
1458 os.path.dirname(
1459 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1460 'git')
1461 if os.path.exists(git_path):
1462 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001463 project = reponame.replace('_', '/')
1464 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001465 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001466 self.setupAllProjectKeys()
1467
1468 def setupAllProjectKeys(self):
1469 if self.create_project_keys:
1470 return
1471
1472 path = self.config.get('zuul', 'tenant_config')
1473 with open(os.path.join(FIXTURE_DIR, path)) as f:
1474 tenant_config = yaml.safe_load(f.read())
1475 for tenant in tenant_config:
1476 sources = tenant['tenant']['source']
1477 for source, conf in sources.items():
1478 for project in conf.get('config-repos', []):
1479 self.setupProjectKeys(source, project)
1480 for project in conf.get('project-repos', []):
1481 self.setupProjectKeys(source, project)
1482
1483 def setupProjectKeys(self, source, project):
1484 # Make sure we set up an RSA key for the project so that we
1485 # don't spend time generating one:
1486
1487 key_root = os.path.join(self.state_root, 'keys')
1488 if not os.path.isdir(key_root):
1489 os.mkdir(key_root, 0o700)
1490 private_key_file = os.path.join(key_root, source, project + '.pem')
1491 private_key_dir = os.path.dirname(private_key_file)
1492 self.log.debug("Installing test keys for project %s at %s" % (
1493 project, private_key_file))
1494 if not os.path.isdir(private_key_dir):
1495 os.makedirs(private_key_dir)
1496 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1497 with open(private_key_file, 'w') as o:
1498 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001499
James E. Blair498059b2016-12-20 13:50:13 -08001500 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001501 self.zk_chroot_fixture = self.useFixture(
1502 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001503 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001504 self.zk_chroot_fixture.zookeeper_host,
1505 self.zk_chroot_fixture.zookeeper_port,
1506 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001507
James E. Blair96c6bf82016-01-15 16:20:40 -08001508 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001509 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001510
1511 files = {}
1512 for (dirpath, dirnames, filenames) in os.walk(source_path):
1513 for filename in filenames:
1514 test_tree_filepath = os.path.join(dirpath, filename)
1515 common_path = os.path.commonprefix([test_tree_filepath,
1516 source_path])
1517 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1518 with open(test_tree_filepath, 'r') as f:
1519 content = f.read()
1520 files[relative_filepath] = content
1521 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001522 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001523
James E. Blaire18d4602017-01-05 11:17:28 -08001524 def assertNodepoolState(self):
1525 # Make sure that there are no pending requests
1526
1527 requests = self.fake_nodepool.getNodeRequests()
1528 self.assertEqual(len(requests), 0)
1529
1530 nodes = self.fake_nodepool.getNodes()
1531 for node in nodes:
1532 self.assertFalse(node['_lock'], "Node %s is locked" %
1533 (node['_oid'],))
1534
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001535 def assertNoGeneratedKeys(self):
1536 # Make sure that Zuul did not generate any project keys
1537 # (unless it was supposed to).
1538
1539 if self.create_project_keys:
1540 return
1541
1542 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1543 test_key = i.read()
1544
1545 key_root = os.path.join(self.state_root, 'keys')
1546 for root, dirname, files in os.walk(key_root):
1547 for fn in files:
1548 with open(os.path.join(root, fn)) as f:
1549 self.assertEqual(test_key, f.read())
1550
Clark Boylanb640e052014-04-03 16:41:46 -07001551 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001552 self.log.debug("Assert final state")
1553 # Make sure no jobs are running
1554 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001555 # Make sure that git.Repo objects have been garbage collected.
1556 repos = []
1557 gc.collect()
1558 for obj in gc.get_objects():
1559 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001560 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001561 repos.append(obj)
1562 self.assertEqual(len(repos), 0)
1563 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001564 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001565 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001566 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001567 for tenant in self.sched.abide.tenants.values():
1568 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001569 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001570 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001571
1572 def shutdown(self):
1573 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001574 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001575 self.merge_server.stop()
1576 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001577 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001578 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001579 self.sched.stop()
1580 self.sched.join()
1581 self.statsd.stop()
1582 self.statsd.join()
1583 self.webapp.stop()
1584 self.webapp.join()
1585 self.rpc.stop()
1586 self.rpc.join()
1587 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001588 self.fake_nodepool.stop()
1589 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001590 threads = threading.enumerate()
1591 if len(threads) > 1:
1592 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001593 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001594
James E. Blaira002b032017-04-18 10:35:48 -07001595 def assertCleanShutdown(self):
1596 pass
1597
Clark Boylanb640e052014-04-03 16:41:46 -07001598 def init_repo(self, project):
1599 parts = project.split('/')
1600 path = os.path.join(self.upstream_root, *parts[:-1])
1601 if not os.path.exists(path):
1602 os.makedirs(path)
1603 path = os.path.join(self.upstream_root, project)
1604 repo = git.Repo.init(path)
1605
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001606 with repo.config_writer() as config_writer:
1607 config_writer.set_value('user', 'email', 'user@example.com')
1608 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001609
Clark Boylanb640e052014-04-03 16:41:46 -07001610 repo.index.commit('initial commit')
1611 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001612
James E. Blair97d902e2014-08-21 13:25:56 -07001613 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001614 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001615 repo.git.clean('-x', '-f', '-d')
1616
James E. Blair97d902e2014-08-21 13:25:56 -07001617 def create_branch(self, project, branch):
1618 path = os.path.join(self.upstream_root, project)
1619 repo = git.Repo.init(path)
1620 fn = os.path.join(path, 'README')
1621
1622 branch_head = repo.create_head(branch)
1623 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001624 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001625 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001626 f.close()
1627 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001628 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001629
James E. Blair97d902e2014-08-21 13:25:56 -07001630 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001631 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001632 repo.git.clean('-x', '-f', '-d')
1633
Sachi King9f16d522016-03-16 12:20:45 +11001634 def create_commit(self, project):
1635 path = os.path.join(self.upstream_root, project)
1636 repo = git.Repo(path)
1637 repo.head.reference = repo.heads['master']
1638 file_name = os.path.join(path, 'README')
1639 with open(file_name, 'a') as f:
1640 f.write('creating fake commit\n')
1641 repo.index.add([file_name])
1642 commit = repo.index.commit('Creating a fake commit')
1643 return commit.hexsha
1644
James E. Blairb8c16472015-05-05 14:55:26 -07001645 def orderedRelease(self):
1646 # Run one build at a time to ensure non-race order:
1647 while len(self.builds):
1648 self.release(self.builds[0])
1649 self.waitUntilSettled()
1650
Clark Boylanb640e052014-04-03 16:41:46 -07001651 def release(self, job):
1652 if isinstance(job, FakeBuild):
1653 job.release()
1654 else:
1655 job.waiting = False
1656 self.log.debug("Queued job %s released" % job.unique)
1657 self.gearman_server.wakeConnections()
1658
1659 def getParameter(self, job, name):
1660 if isinstance(job, FakeBuild):
1661 return job.parameters[name]
1662 else:
1663 parameters = json.loads(job.arguments)
1664 return parameters[name]
1665
Clark Boylanb640e052014-04-03 16:41:46 -07001666 def haveAllBuildsReported(self):
1667 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001668 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001669 return False
1670 # Find out if every build that the worker has completed has been
1671 # reported back to Zuul. If it hasn't then that means a Gearman
1672 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001673 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001674 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001675 if not zbuild:
1676 # It has already been reported
1677 continue
1678 # It hasn't been reported yet.
1679 return False
1680 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001681 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001682 if connection.state == 'GRAB_WAIT':
1683 return False
1684 return True
1685
1686 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001687 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001688 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001689 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001690 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001691 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001692 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001693 for j in conn.related_jobs.values():
1694 if j.unique == build.uuid:
1695 client_job = j
1696 break
1697 if not client_job:
1698 self.log.debug("%s is not known to the gearman client" %
1699 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001700 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001701 if not client_job.handle:
1702 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001703 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001704 server_job = self.gearman_server.jobs.get(client_job.handle)
1705 if not server_job:
1706 self.log.debug("%s is not known to the gearman server" %
1707 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001708 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001709 if not hasattr(server_job, 'waiting'):
1710 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001711 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001712 if server_job.waiting:
1713 continue
James E. Blair17302972016-08-10 16:11:42 -07001714 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001715 self.log.debug("%s has not reported start" % build)
1716 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001717 worker_build = self.executor_server.job_builds.get(
1718 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001719 if worker_build:
1720 if worker_build.isWaiting():
1721 continue
1722 else:
1723 self.log.debug("%s is running" % worker_build)
1724 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001725 else:
James E. Blair962220f2016-08-03 11:22:38 -07001726 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001727 return False
James E. Blaira002b032017-04-18 10:35:48 -07001728 for (build_uuid, job_worker) in \
1729 self.executor_server.job_workers.items():
1730 if build_uuid not in seen_builds:
1731 self.log.debug("%s is not finalized" % build_uuid)
1732 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001733 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001734
James E. Blairdce6cea2016-12-20 16:45:32 -08001735 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001736 if self.fake_nodepool.paused:
1737 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001738 if self.sched.nodepool.requests:
1739 return False
1740 return True
1741
Jan Hruban6b71aff2015-10-22 16:58:08 +02001742 def eventQueuesEmpty(self):
1743 for queue in self.event_queues:
1744 yield queue.empty()
1745
1746 def eventQueuesJoin(self):
1747 for queue in self.event_queues:
1748 queue.join()
1749
Clark Boylanb640e052014-04-03 16:41:46 -07001750 def waitUntilSettled(self):
1751 self.log.debug("Waiting until settled...")
1752 start = time.time()
1753 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001754 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001755 self.log.error("Timeout waiting for Zuul to settle")
1756 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001757 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001758 self.log.error(" %s: %s" % (queue, queue.empty()))
1759 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001760 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001761 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001762 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001763 self.log.error("All requests completed: %s" %
1764 (self.areAllNodeRequestsComplete(),))
1765 self.log.error("Merge client jobs: %s" %
1766 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001767 raise Exception("Timeout waiting for Zuul to settle")
1768 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001769
Paul Belanger174a8272017-03-14 13:20:10 -04001770 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001771 # have all build states propogated to zuul?
1772 if self.haveAllBuildsReported():
1773 # Join ensures that the queue is empty _and_ events have been
1774 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001775 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001776 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001777 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001778 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001779 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001780 self.areAllNodeRequestsComplete() and
1781 all(self.eventQueuesEmpty())):
1782 # The queue empty check is placed at the end to
1783 # ensure that if a component adds an event between
1784 # when locked the run handler and checked that the
1785 # components were stable, we don't erroneously
1786 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001787 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001788 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001789 self.log.debug("...settled.")
1790 return
1791 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001792 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001793 self.sched.wake_event.wait(0.1)
1794
1795 def countJobResults(self, jobs, result):
1796 jobs = filter(lambda x: x.result == result, jobs)
1797 return len(jobs)
1798
James E. Blair96c6bf82016-01-15 16:20:40 -08001799 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001800 for job in self.history:
1801 if (job.name == name and
1802 (project is None or
1803 job.parameters['ZUUL_PROJECT'] == project)):
1804 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001805 raise Exception("Unable to find job %s in history" % name)
1806
1807 def assertEmptyQueues(self):
1808 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001809 for tenant in self.sched.abide.tenants.values():
1810 for pipeline in tenant.layout.pipelines.values():
1811 for queue in pipeline.queues:
1812 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001813 print('pipeline %s queue %s contents %s' % (
1814 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001815 self.assertEqual(len(queue.queue), 0,
1816 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001817
1818 def assertReportedStat(self, key, value=None, kind=None):
1819 start = time.time()
1820 while time.time() < (start + 5):
1821 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001822 k, v = stat.split(':')
1823 if key == k:
1824 if value is None and kind is None:
1825 return
1826 elif value:
1827 if value == v:
1828 return
1829 elif kind:
1830 if v.endswith('|' + kind):
1831 return
1832 time.sleep(0.1)
1833
Clark Boylanb640e052014-04-03 16:41:46 -07001834 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001835
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001836 def assertBuilds(self, builds):
1837 """Assert that the running builds are as described.
1838
1839 The list of running builds is examined and must match exactly
1840 the list of builds described by the input.
1841
1842 :arg list builds: A list of dictionaries. Each item in the
1843 list must match the corresponding build in the build
1844 history, and each element of the dictionary must match the
1845 corresponding attribute of the build.
1846
1847 """
James E. Blair3158e282016-08-19 09:34:11 -07001848 try:
1849 self.assertEqual(len(self.builds), len(builds))
1850 for i, d in enumerate(builds):
1851 for k, v in d.items():
1852 self.assertEqual(
1853 getattr(self.builds[i], k), v,
1854 "Element %i in builds does not match" % (i,))
1855 except Exception:
1856 for build in self.builds:
1857 self.log.error("Running build: %s" % build)
1858 else:
1859 self.log.error("No running builds")
1860 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001861
James E. Blairb536ecc2016-08-31 10:11:42 -07001862 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001863 """Assert that the completed builds are as described.
1864
1865 The list of completed builds is examined and must match
1866 exactly the list of builds described by the input.
1867
1868 :arg list history: A list of dictionaries. Each item in the
1869 list must match the corresponding build in the build
1870 history, and each element of the dictionary must match the
1871 corresponding attribute of the build.
1872
James E. Blairb536ecc2016-08-31 10:11:42 -07001873 :arg bool ordered: If true, the history must match the order
1874 supplied, if false, the builds are permitted to have
1875 arrived in any order.
1876
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001877 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001878 def matches(history_item, item):
1879 for k, v in item.items():
1880 if getattr(history_item, k) != v:
1881 return False
1882 return True
James E. Blair3158e282016-08-19 09:34:11 -07001883 try:
1884 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001885 if ordered:
1886 for i, d in enumerate(history):
1887 if not matches(self.history[i], d):
1888 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001889 "Element %i in history does not match %s" %
1890 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001891 else:
1892 unseen = self.history[:]
1893 for i, d in enumerate(history):
1894 found = False
1895 for unseen_item in unseen:
1896 if matches(unseen_item, d):
1897 found = True
1898 unseen.remove(unseen_item)
1899 break
1900 if not found:
1901 raise Exception("No match found for element %i "
1902 "in history" % (i,))
1903 if unseen:
1904 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001905 except Exception:
1906 for build in self.history:
1907 self.log.error("Completed build: %s" % build)
1908 else:
1909 self.log.error("No completed builds")
1910 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001911
James E. Blair6ac368c2016-12-22 18:07:20 -08001912 def printHistory(self):
1913 """Log the build history.
1914
1915 This can be useful during tests to summarize what jobs have
1916 completed.
1917
1918 """
1919 self.log.debug("Build history:")
1920 for build in self.history:
1921 self.log.debug(build)
1922
James E. Blair59fdbac2015-12-07 17:08:06 -08001923 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001924 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1925
1926 def updateConfigLayout(self, path):
1927 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001928 if not os.path.exists(root):
1929 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001930 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1931 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001932- tenant:
1933 name: openstack
1934 source:
1935 gerrit:
1936 config-repos:
1937 - %s
1938 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001939 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001940 self.config.set('zuul', 'tenant_config',
1941 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001942 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08001943
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001944 def addCommitToRepo(self, project, message, files,
1945 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001946 path = os.path.join(self.upstream_root, project)
1947 repo = git.Repo(path)
1948 repo.head.reference = branch
1949 zuul.merger.merger.reset_repo_to_head(repo)
1950 for fn, content in files.items():
1951 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001952 try:
1953 os.makedirs(os.path.dirname(fn))
1954 except OSError:
1955 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001956 with open(fn, 'w') as f:
1957 f.write(content)
1958 repo.index.add([fn])
1959 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001960 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001961 repo.heads[branch].commit = commit
1962 repo.head.reference = branch
1963 repo.git.clean('-x', '-f', '-d')
1964 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001965 if tag:
1966 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001967 return before
1968
1969 def commitLayoutUpdate(self, orig_name, source_name):
1970 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07001971 source_name)
1972 to_copy = ['zuul.yaml']
1973 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
1974 to_copy.append('playbooks/{}'.format(playbook))
1975 commit_data = {}
1976 for source_file in to_copy:
1977 source_file_path = os.path.join(source_path, source_file)
1978 with open(source_file_path, 'r') as nt:
1979 commit_data[source_file] = nt.read()
1980 before = self.addCommitToRepo(
1981 orig_name, 'Pulling content from %s' % source_name,
1982 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08001983 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001984
James E. Blair7fc8daa2016-08-08 15:37:15 -07001985 def addEvent(self, connection, event):
1986 """Inject a Fake (Gerrit) event.
1987
1988 This method accepts a JSON-encoded event and simulates Zuul
1989 having received it from Gerrit. It could (and should)
1990 eventually apply to any connection type, but is currently only
1991 used with Gerrit connections. The name of the connection is
1992 used to look up the corresponding server, and the event is
1993 simulated as having been received by all Zuul connections
1994 attached to that server. So if two Gerrit connections in Zuul
1995 are connected to the same Gerrit server, and you invoke this
1996 method specifying the name of one of them, the event will be
1997 received by both.
1998
1999 .. note::
2000
2001 "self.fake_gerrit.addEvent" calls should be migrated to
2002 this method.
2003
2004 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002005 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002006 :arg str event: The JSON-encoded event.
2007
2008 """
2009 specified_conn = self.connections.connections[connection]
2010 for conn in self.connections.connections.values():
2011 if (isinstance(conn, specified_conn.__class__) and
2012 specified_conn.server == conn.server):
2013 conn.addEvent(event)
2014
James E. Blair3f876d52016-07-22 13:07:14 -07002015
2016class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002017 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002018 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002019
Joshua Heskethd78b4482015-09-14 16:56:34 -06002020
2021class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002022 def setup_config(self):
2023 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002024 for section_name in self.config.sections():
2025 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2026 section_name, re.I)
2027 if not con_match:
2028 continue
2029
2030 if self.config.get(section_name, 'driver') == 'sql':
2031 f = MySQLSchemaFixture()
2032 self.useFixture(f)
2033 if (self.config.get(section_name, 'dburi') ==
2034 '$MYSQL_FIXTURE_DBURI$'):
2035 self.config.set(section_name, 'dburi', f.dburi)