blob: 29981acff481e5d40d95e757af9dd02ac7c0a18a [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
Clark Boylanb640e052014-04-03 16:41:46 -070053
James E. Blaire511d2f2016-12-08 15:22:26 -080054import zuul.driver.gerrit.gerritsource as gerritsource
55import zuul.driver.gerrit.gerritconnection as gerritconnection
Clark Boylanb640e052014-04-03 16:41:46 -070056import zuul.scheduler
57import zuul.webapp
58import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040059import zuul.executor.server
60import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080061import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070063import zuul.merger.merger
64import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070065import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080066import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070067
68FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
69 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080070
71KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070072
Clark Boylanb640e052014-04-03 16:41:46 -070073
74def repack_repo(path):
75 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
76 output = subprocess.Popen(cmd, close_fds=True,
77 stdout=subprocess.PIPE,
78 stderr=subprocess.PIPE)
79 out = output.communicate()
80 if output.returncode:
81 raise Exception("git repack returned %d" % output.returncode)
82 return out
83
84
85def random_sha1():
86 return hashlib.sha1(str(random.random())).hexdigest()
87
88
James E. Blaira190f3b2015-01-05 14:56:54 -080089def iterate_timeout(max_seconds, purpose):
90 start = time.time()
91 count = 0
92 while (time.time() < start + max_seconds):
93 count += 1
94 yield count
95 time.sleep(0)
96 raise Exception("Timeout waiting for %s" % purpose)
97
98
Clark Boylanb640e052014-04-03 16:41:46 -070099class ChangeReference(git.Reference):
100 _common_path_default = "refs/changes"
101 _points_to_commits_only = True
102
103
104class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700105 categories = {'approved': ('Approved', -1, 1),
106 'code-review': ('Code-Review', -2, 2),
107 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700108
109 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700110 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700111 self.gerrit = gerrit
112 self.reported = 0
113 self.queried = 0
114 self.patchsets = []
115 self.number = number
116 self.project = project
117 self.branch = branch
118 self.subject = subject
119 self.latest_patchset = 0
120 self.depends_on_change = None
121 self.needed_by_changes = []
122 self.fail_merge = False
123 self.messages = []
124 self.data = {
125 'branch': branch,
126 'comments': [],
127 'commitMessage': subject,
128 'createdOn': time.time(),
129 'id': 'I' + random_sha1(),
130 'lastUpdated': time.time(),
131 'number': str(number),
132 'open': status == 'NEW',
133 'owner': {'email': 'user@example.com',
134 'name': 'User Name',
135 'username': 'username'},
136 'patchSets': self.patchsets,
137 'project': project,
138 'status': status,
139 'subject': subject,
140 'submitRecords': [],
141 'url': 'https://hostname/%s' % number}
142
143 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700144 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.data['submitRecords'] = self.getSubmitRecords()
146 self.open = status == 'NEW'
147
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700148 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700149 path = os.path.join(self.upstream_root, self.project)
150 repo = git.Repo(path)
151 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
152 self.latest_patchset),
153 'refs/tags/init')
154 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700155 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700156 repo.git.clean('-x', '-f', '-d')
157
158 path = os.path.join(self.upstream_root, self.project)
159 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700160 for fn, content in files.items():
161 fn = os.path.join(path, fn)
162 with open(fn, 'w') as f:
163 f.write(content)
164 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700165 else:
166 for fni in range(100):
167 fn = os.path.join(path, str(fni))
168 f = open(fn, 'w')
169 for ci in range(4096):
170 f.write(random.choice(string.printable))
171 f.close()
172 repo.index.add([fn])
173
174 r = repo.index.commit(msg)
175 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700176 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 repo.git.clean('-x', '-f', '-d')
178 repo.heads['master'].checkout()
179 return r
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700183 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700184 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700185 data = ("test %s %s %s\n" %
186 (self.branch, self.number, self.latest_patchset))
187 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700188 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700189 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 ps_files = [{'file': '/COMMIT_MSG',
191 'type': 'ADDED'},
192 {'file': 'README',
193 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700195 ps_files.append({'file': f, 'type': 'ADDED'})
196 d = {'approvals': [],
197 'createdOn': time.time(),
198 'files': ps_files,
199 'number': str(self.latest_patchset),
200 'ref': 'refs/changes/1/%s/%s' % (self.number,
201 self.latest_patchset),
202 'revision': c.hexsha,
203 'uploader': {'email': 'user@example.com',
204 'name': 'User name',
205 'username': 'user'}}
206 self.data['currentPatchSet'] = d
207 self.patchsets.append(d)
208 self.data['submitRecords'] = self.getSubmitRecords()
209
210 def getPatchsetCreatedEvent(self, patchset):
211 event = {"type": "patchset-created",
212 "change": {"project": self.project,
213 "branch": self.branch,
214 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
215 "number": str(self.number),
216 "subject": self.subject,
217 "owner": {"name": "User Name"},
218 "url": "https://hostname/3"},
219 "patchSet": self.patchsets[patchset - 1],
220 "uploader": {"name": "User Name"}}
221 return event
222
223 def getChangeRestoredEvent(self):
224 event = {"type": "change-restored",
225 "change": {"project": self.project,
226 "branch": self.branch,
227 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
228 "number": str(self.number),
229 "subject": self.subject,
230 "owner": {"name": "User Name"},
231 "url": "https://hostname/3"},
232 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100233 "patchSet": self.patchsets[-1],
234 "reason": ""}
235 return event
236
237 def getChangeAbandonedEvent(self):
238 event = {"type": "change-abandoned",
239 "change": {"project": self.project,
240 "branch": self.branch,
241 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
242 "number": str(self.number),
243 "subject": self.subject,
244 "owner": {"name": "User Name"},
245 "url": "https://hostname/3"},
246 "abandoner": {"name": "User Name"},
247 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700248 "reason": ""}
249 return event
250
251 def getChangeCommentEvent(self, patchset):
252 event = {"type": "comment-added",
253 "change": {"project": self.project,
254 "branch": self.branch,
255 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
256 "number": str(self.number),
257 "subject": self.subject,
258 "owner": {"name": "User Name"},
259 "url": "https://hostname/3"},
260 "patchSet": self.patchsets[patchset - 1],
261 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700262 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700263 "description": "Code-Review",
264 "value": "0"}],
265 "comment": "This is a comment"}
266 return event
267
James E. Blairc2a5ed72017-02-20 14:12:01 -0500268 def getChangeMergedEvent(self):
269 event = {"submitter": {"name": "Jenkins",
270 "username": "jenkins"},
271 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
272 "patchSet": self.patchsets[-1],
273 "change": self.data,
274 "type": "change-merged",
275 "eventCreatedOn": 1487613810}
276 return event
277
James E. Blair8cce42e2016-10-18 08:18:36 -0700278 def getRefUpdatedEvent(self):
279 path = os.path.join(self.upstream_root, self.project)
280 repo = git.Repo(path)
281 oldrev = repo.heads[self.branch].commit.hexsha
282
283 event = {
284 "type": "ref-updated",
285 "submitter": {
286 "name": "User Name",
287 },
288 "refUpdate": {
289 "oldRev": oldrev,
290 "newRev": self.patchsets[-1]['revision'],
291 "refName": self.branch,
292 "project": self.project,
293 }
294 }
295 return event
296
Joshua Hesketh642824b2014-07-01 17:54:59 +1000297 def addApproval(self, category, value, username='reviewer_john',
298 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700299 if not granted_on:
300 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000301 approval = {
302 'description': self.categories[category][0],
303 'type': category,
304 'value': str(value),
305 'by': {
306 'username': username,
307 'email': username + '@example.com',
308 },
309 'grantedOn': int(granted_on)
310 }
Clark Boylanb640e052014-04-03 16:41:46 -0700311 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
312 if x['by']['username'] == username and x['type'] == category:
313 del self.patchsets[-1]['approvals'][i]
314 self.patchsets[-1]['approvals'].append(approval)
315 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000316 'author': {'email': 'author@example.com',
317 'name': 'Patchset Author',
318 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700319 'change': {'branch': self.branch,
320 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
321 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000322 'owner': {'email': 'owner@example.com',
323 'name': 'Change Owner',
324 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700325 'project': self.project,
326 'subject': self.subject,
327 'topic': 'master',
328 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700330 'patchSet': self.patchsets[-1],
331 'type': 'comment-added'}
332 self.data['submitRecords'] = self.getSubmitRecords()
333 return json.loads(json.dumps(event))
334
335 def getSubmitRecords(self):
336 status = {}
337 for cat in self.categories.keys():
338 status[cat] = 0
339
340 for a in self.patchsets[-1]['approvals']:
341 cur = status[a['type']]
342 cat_min, cat_max = self.categories[a['type']][1:]
343 new = int(a['value'])
344 if new == cat_min:
345 cur = new
346 elif abs(new) > abs(cur):
347 cur = new
348 status[a['type']] = cur
349
350 labels = []
351 ok = True
352 for typ, cat in self.categories.items():
353 cur = status[typ]
354 cat_min, cat_max = cat[1:]
355 if cur == cat_min:
356 value = 'REJECT'
357 ok = False
358 elif cur == cat_max:
359 value = 'OK'
360 else:
361 value = 'NEED'
362 ok = False
363 labels.append({'label': cat[0], 'status': value})
364 if ok:
365 return [{'status': 'OK'}]
366 return [{'status': 'NOT_READY',
367 'labels': labels}]
368
369 def setDependsOn(self, other, patchset):
370 self.depends_on_change = other
371 d = {'id': other.data['id'],
372 'number': other.data['number'],
373 'ref': other.patchsets[patchset - 1]['ref']
374 }
375 self.data['dependsOn'] = [d]
376
377 other.needed_by_changes.append(self)
378 needed = other.data.get('neededBy', [])
379 d = {'id': self.data['id'],
380 'number': self.data['number'],
381 'ref': self.patchsets[patchset - 1]['ref'],
382 'revision': self.patchsets[patchset - 1]['revision']
383 }
384 needed.append(d)
385 other.data['neededBy'] = needed
386
387 def query(self):
388 self.queried += 1
389 d = self.data.get('dependsOn')
390 if d:
391 d = d[0]
392 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
393 d['isCurrentPatchSet'] = True
394 else:
395 d['isCurrentPatchSet'] = False
396 return json.loads(json.dumps(self.data))
397
398 def setMerged(self):
399 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000400 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700401 return
402 if self.fail_merge:
403 return
404 self.data['status'] = 'MERGED'
405 self.open = False
406
407 path = os.path.join(self.upstream_root, self.project)
408 repo = git.Repo(path)
409 repo.heads[self.branch].commit = \
410 repo.commit(self.patchsets[-1]['revision'])
411
412 def setReported(self):
413 self.reported += 1
414
415
James E. Blaire511d2f2016-12-08 15:22:26 -0800416class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700417 """A Fake Gerrit connection for use in tests.
418
419 This subclasses
420 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
421 ability for tests to add changes to the fake Gerrit it represents.
422 """
423
Joshua Hesketh352264b2015-08-11 23:42:08 +1000424 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700425
James E. Blaire511d2f2016-12-08 15:22:26 -0800426 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700427 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800428 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000429 connection_config)
430
James E. Blair7fc8daa2016-08-08 15:37:15 -0700431 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700432 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
433 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000434 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700435 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200436 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700437
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700438 def addFakeChange(self, project, branch, subject, status='NEW',
439 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700440 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700441 self.change_number += 1
442 c = FakeChange(self, self.change_number, project, branch, subject,
443 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700444 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700445 self.changes[self.change_number] = c
446 return c
447
Clark Boylanb640e052014-04-03 16:41:46 -0700448 def review(self, project, changeid, message, action):
449 number, ps = changeid.split(',')
450 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000451
452 # Add the approval back onto the change (ie simulate what gerrit would
453 # do).
454 # Usually when zuul leaves a review it'll create a feedback loop where
455 # zuul's review enters another gerrit event (which is then picked up by
456 # zuul). However, we can't mimic this behaviour (by adding this
457 # approval event into the queue) as it stops jobs from checking what
458 # happens before this event is triggered. If a job needs to see what
459 # happens they can add their own verified event into the queue.
460 # Nevertheless, we can update change with the new review in gerrit.
461
James E. Blair8b5408c2016-08-08 15:37:46 -0700462 for cat in action.keys():
463 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000464 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000465
James E. Blair8b5408c2016-08-08 15:37:46 -0700466 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000467 if 'label' in action:
468 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000469 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000470
Clark Boylanb640e052014-04-03 16:41:46 -0700471 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000472
Clark Boylanb640e052014-04-03 16:41:46 -0700473 if 'submit' in action:
474 change.setMerged()
475 if message:
476 change.setReported()
477
478 def query(self, number):
479 change = self.changes.get(int(number))
480 if change:
481 return change.query()
482 return {}
483
James E. Blairc494d542014-08-06 09:23:52 -0700484 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700485 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700486 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800487 if query.startswith('change:'):
488 # Query a specific changeid
489 changeid = query[len('change:'):]
490 l = [change.query() for change in self.changes.values()
491 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700492 elif query.startswith('message:'):
493 # Query the content of a commit message
494 msg = query[len('message:'):].strip()
495 l = [change.query() for change in self.changes.values()
496 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800497 else:
498 # Query all open changes
499 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700500 return l
James E. Blairc494d542014-08-06 09:23:52 -0700501
Joshua Hesketh352264b2015-08-11 23:42:08 +1000502 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700503 pass
504
Joshua Hesketh352264b2015-08-11 23:42:08 +1000505 def getGitUrl(self, project):
506 return os.path.join(self.upstream_root, project.name)
507
Clark Boylanb640e052014-04-03 16:41:46 -0700508
509class BuildHistory(object):
510 def __init__(self, **kw):
511 self.__dict__.update(kw)
512
513 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700514 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
515 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700516
517
518class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200519 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700520 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700521 self.url = url
522
523 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700524 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700525 path = res.path
526 project = '/'.join(path.split('/')[2:-2])
527 ret = '001e# service=git-upload-pack\n'
528 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
529 'multi_ack thin-pack side-band side-band-64k ofs-delta '
530 'shallow no-progress include-tag multi_ack_detailed no-done\n')
531 path = os.path.join(self.upstream_root, project)
532 repo = git.Repo(path)
533 for ref in repo.refs:
534 r = ref.object.hexsha + ' ' + ref.path + '\n'
535 ret += '%04x%s' % (len(r) + 4, r)
536 ret += '0000'
537 return ret
538
539
Clark Boylanb640e052014-04-03 16:41:46 -0700540class FakeStatsd(threading.Thread):
541 def __init__(self):
542 threading.Thread.__init__(self)
543 self.daemon = True
544 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
545 self.sock.bind(('', 0))
546 self.port = self.sock.getsockname()[1]
547 self.wake_read, self.wake_write = os.pipe()
548 self.stats = []
549
550 def run(self):
551 while True:
552 poll = select.poll()
553 poll.register(self.sock, select.POLLIN)
554 poll.register(self.wake_read, select.POLLIN)
555 ret = poll.poll()
556 for (fd, event) in ret:
557 if fd == self.sock.fileno():
558 data = self.sock.recvfrom(1024)
559 if not data:
560 return
561 self.stats.append(data[0])
562 if fd == self.wake_read:
563 return
564
565 def stop(self):
566 os.write(self.wake_write, '1\n')
567
568
James E. Blaire1767bc2016-08-02 10:00:27 -0700569class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700570 log = logging.getLogger("zuul.test")
571
Paul Belanger174a8272017-03-14 13:20:10 -0400572 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700573 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400574 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700575 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700576 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700577 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700578 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700579 # TODOv3(jeblair): self.node is really "the image of the node
580 # assigned". We should rename it (self.node_image?) if we
581 # keep using it like this, or we may end up exposing more of
582 # the complexity around multi-node jobs here
583 # (self.nodes[0].image?)
584 self.node = None
585 if len(self.parameters.get('nodes')) == 1:
586 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700587 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100588 self.pipeline = self.parameters['ZUUL_PIPELINE']
589 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700590 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700591 self.wait_condition = threading.Condition()
592 self.waiting = False
593 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500594 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700595 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700596 self.changes = None
597 if 'ZUUL_CHANGE_IDS' in self.parameters:
598 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700599
James E. Blair3158e282016-08-19 09:34:11 -0700600 def __repr__(self):
601 waiting = ''
602 if self.waiting:
603 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100604 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
605 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700606
Clark Boylanb640e052014-04-03 16:41:46 -0700607 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700608 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700609 self.wait_condition.acquire()
610 self.wait_condition.notify()
611 self.waiting = False
612 self.log.debug("Build %s released" % self.unique)
613 self.wait_condition.release()
614
615 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700616 """Return whether this build is being held.
617
618 :returns: Whether the build is being held.
619 :rtype: bool
620 """
621
Clark Boylanb640e052014-04-03 16:41:46 -0700622 self.wait_condition.acquire()
623 if self.waiting:
624 ret = True
625 else:
626 ret = False
627 self.wait_condition.release()
628 return ret
629
630 def _wait(self):
631 self.wait_condition.acquire()
632 self.waiting = True
633 self.log.debug("Build %s waiting" % self.unique)
634 self.wait_condition.wait()
635 self.wait_condition.release()
636
637 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700638 self.log.debug('Running build %s' % self.unique)
639
Paul Belanger174a8272017-03-14 13:20:10 -0400640 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700641 self.log.debug('Holding build %s' % self.unique)
642 self._wait()
643 self.log.debug("Build %s continuing" % self.unique)
644
James E. Blair412fba82017-01-26 15:00:50 -0800645 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700646 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800647 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700648 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800649 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500650 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800651 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700652
James E. Blaire1767bc2016-08-02 10:00:27 -0700653 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700654
James E. Blaira5dba232016-08-08 15:53:24 -0700655 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400656 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700657 for change in changes:
658 if self.hasChanges(change):
659 return True
660 return False
661
James E. Blaire7b99a02016-08-05 14:27:34 -0700662 def hasChanges(self, *changes):
663 """Return whether this build has certain changes in its git repos.
664
665 :arg FakeChange changes: One or more changes (varargs) that
666 are expected to be present (in order) in the git repository of
667 the active project.
668
669 :returns: Whether the build has the indicated changes.
670 :rtype: bool
671
672 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800673 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500674 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800675 try:
676 repo = git.Repo(path)
677 except NoSuchPathError as e:
678 self.log.debug('%s' % e)
679 return False
680 ref = self.parameters['ZUUL_REF']
681 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
682 commit_message = '%s-1' % change.subject
683 self.log.debug("Checking if build %s has changes; commit_message "
684 "%s; repo_messages %s" % (self, commit_message,
685 repo_messages))
686 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700687 self.log.debug(" messages do not match")
688 return False
689 self.log.debug(" OK")
690 return True
691
Clark Boylanb640e052014-04-03 16:41:46 -0700692
Paul Belanger174a8272017-03-14 13:20:10 -0400693class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
694 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700695
Paul Belanger174a8272017-03-14 13:20:10 -0400696 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700697 they will report that they have started but then pause until
698 released before reporting completion. This attribute may be
699 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400700 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700701 be explicitly released.
702
703 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800704 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700705 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800706 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400707 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700708 self.hold_jobs_in_build = False
709 self.lock = threading.Lock()
710 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700711 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700712 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700713 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800714
James E. Blaira5dba232016-08-08 15:53:24 -0700715 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400716 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700717
718 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700719 :arg Change change: The :py:class:`~tests.base.FakeChange`
720 instance which should cause the job to fail. This job
721 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700722
723 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700724 l = self.fail_tests.get(name, [])
725 l.append(change)
726 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800727
James E. Blair962220f2016-08-03 11:22:38 -0700728 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700729 """Release a held build.
730
731 :arg str regex: A regular expression which, if supplied, will
732 cause only builds with matching names to be released. If
733 not supplied, all builds will be released.
734
735 """
James E. Blair962220f2016-08-03 11:22:38 -0700736 builds = self.running_builds[:]
737 self.log.debug("Releasing build %s (%s)" % (regex,
738 len(self.running_builds)))
739 for build in builds:
740 if not regex or re.match(regex, build.name):
741 self.log.debug("Releasing build %s" %
742 (build.parameters['ZUUL_UUID']))
743 build.release()
744 else:
745 self.log.debug("Not releasing build %s" %
746 (build.parameters['ZUUL_UUID']))
747 self.log.debug("Done releasing builds %s (%s)" %
748 (regex, len(self.running_builds)))
749
Paul Belanger174a8272017-03-14 13:20:10 -0400750 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700751 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700752 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700753 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700754 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800755 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500756 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800757 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100758 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
759 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700760
761 def stopJob(self, job):
762 self.log.debug("handle stop")
763 parameters = json.loads(job.arguments)
764 uuid = parameters['uuid']
765 for build in self.running_builds:
766 if build.unique == uuid:
767 build.aborted = True
768 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400769 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700770
Joshua Hesketh50c21782016-10-13 21:34:14 +1100771
Paul Belanger174a8272017-03-14 13:20:10 -0400772class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
Paul Belanger96618ed2017-03-01 09:42:33 -0500773 def runPlaybooks(self, args):
Paul Belanger174a8272017-03-14 13:20:10 -0400774 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800775 build.jobdir = self.jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700776
Paul Belanger96618ed2017-03-01 09:42:33 -0500777 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
James E. Blair412fba82017-01-26 15:00:50 -0800778
Paul Belanger174a8272017-03-14 13:20:10 -0400779 self.executor_server.lock.acquire()
780 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700781 BuildHistory(name=build.name, result=result, changes=build.changes,
782 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800783 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700784 pipeline=build.parameters['ZUUL_PIPELINE'])
785 )
Paul Belanger174a8272017-03-14 13:20:10 -0400786 self.executor_server.running_builds.remove(build)
787 del self.executor_server.job_builds[self.job.unique]
788 self.executor_server.lock.release()
James E. Blair412fba82017-01-26 15:00:50 -0800789 return result
790
Monty Taylore6562aa2017-02-20 07:37:39 -0500791 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400792 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800793
Paul Belanger174a8272017-03-14 13:20:10 -0400794 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600795 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500796 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800797 else:
798 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700799 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800800
James E. Blairad8dca02017-02-21 11:48:32 -0500801 def getHostList(self, args):
802 self.log.debug("hostlist")
803 hosts = super(RecordingAnsibleJob, self).getHostList(args)
804 for name, d in hosts:
805 d['ansible_connection'] = 'local'
806 hosts.append(('localhost', dict(ansible_connection='local')))
807 return hosts
808
James E. Blairf5dbd002015-12-23 15:26:17 -0800809
Clark Boylanb640e052014-04-03 16:41:46 -0700810class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700811 """A Gearman server for use in tests.
812
813 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
814 added to the queue but will not be distributed to workers
815 until released. This attribute may be changed at any time and
816 will take effect for subsequently enqueued jobs, but
817 previously held jobs will still need to be explicitly
818 released.
819
820 """
821
Clark Boylanb640e052014-04-03 16:41:46 -0700822 def __init__(self):
823 self.hold_jobs_in_queue = False
824 super(FakeGearmanServer, self).__init__(0)
825
826 def getJobForConnection(self, connection, peek=False):
827 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
828 for job in queue:
829 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400830 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700831 job.waiting = self.hold_jobs_in_queue
832 else:
833 job.waiting = False
834 if job.waiting:
835 continue
836 if job.name in connection.functions:
837 if not peek:
838 queue.remove(job)
839 connection.related_jobs[job.handle] = job
840 job.worker_connection = connection
841 job.running = True
842 return job
843 return None
844
845 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700846 """Release a held job.
847
848 :arg str regex: A regular expression which, if supplied, will
849 cause only jobs with matching names to be released. If
850 not supplied, all jobs will be released.
851 """
Clark Boylanb640e052014-04-03 16:41:46 -0700852 released = False
853 qlen = (len(self.high_queue) + len(self.normal_queue) +
854 len(self.low_queue))
855 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
856 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400857 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700858 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500859 parameters = json.loads(job.arguments)
860 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700861 self.log.debug("releasing queued job %s" %
862 job.unique)
863 job.waiting = False
864 released = True
865 else:
866 self.log.debug("not releasing queued job %s" %
867 job.unique)
868 if released:
869 self.wakeConnections()
870 qlen = (len(self.high_queue) + len(self.normal_queue) +
871 len(self.low_queue))
872 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
873
874
875class FakeSMTP(object):
876 log = logging.getLogger('zuul.FakeSMTP')
877
878 def __init__(self, messages, server, port):
879 self.server = server
880 self.port = port
881 self.messages = messages
882
883 def sendmail(self, from_email, to_email, msg):
884 self.log.info("Sending email from %s, to %s, with msg %s" % (
885 from_email, to_email, msg))
886
887 headers = msg.split('\n\n', 1)[0]
888 body = msg.split('\n\n', 1)[1]
889
890 self.messages.append(dict(
891 from_email=from_email,
892 to_email=to_email,
893 msg=msg,
894 headers=headers,
895 body=body,
896 ))
897
898 return True
899
900 def quit(self):
901 return True
902
903
James E. Blairdce6cea2016-12-20 16:45:32 -0800904class FakeNodepool(object):
905 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800906 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800907
908 log = logging.getLogger("zuul.test.FakeNodepool")
909
910 def __init__(self, host, port, chroot):
911 self.client = kazoo.client.KazooClient(
912 hosts='%s:%s%s' % (host, port, chroot))
913 self.client.start()
914 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800915 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800916 self.thread = threading.Thread(target=self.run)
917 self.thread.daemon = True
918 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800919 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800920
921 def stop(self):
922 self._running = False
923 self.thread.join()
924 self.client.stop()
925 self.client.close()
926
927 def run(self):
928 while self._running:
929 self._run()
930 time.sleep(0.1)
931
932 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800933 if self.paused:
934 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800935 for req in self.getNodeRequests():
936 self.fulfillRequest(req)
937
938 def getNodeRequests(self):
939 try:
940 reqids = self.client.get_children(self.REQUEST_ROOT)
941 except kazoo.exceptions.NoNodeError:
942 return []
943 reqs = []
944 for oid in sorted(reqids):
945 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800946 try:
947 data, stat = self.client.get(path)
948 data = json.loads(data)
949 data['_oid'] = oid
950 reqs.append(data)
951 except kazoo.exceptions.NoNodeError:
952 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800953 return reqs
954
James E. Blaire18d4602017-01-05 11:17:28 -0800955 def getNodes(self):
956 try:
957 nodeids = self.client.get_children(self.NODE_ROOT)
958 except kazoo.exceptions.NoNodeError:
959 return []
960 nodes = []
961 for oid in sorted(nodeids):
962 path = self.NODE_ROOT + '/' + oid
963 data, stat = self.client.get(path)
964 data = json.loads(data)
965 data['_oid'] = oid
966 try:
967 lockfiles = self.client.get_children(path + '/lock')
968 except kazoo.exceptions.NoNodeError:
969 lockfiles = []
970 if lockfiles:
971 data['_lock'] = True
972 else:
973 data['_lock'] = False
974 nodes.append(data)
975 return nodes
976
James E. Blaira38c28e2017-01-04 10:33:20 -0800977 def makeNode(self, request_id, node_type):
978 now = time.time()
979 path = '/nodepool/nodes/'
980 data = dict(type=node_type,
981 provider='test-provider',
982 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -0400983 az='test-az',
James E. Blaira38c28e2017-01-04 10:33:20 -0800984 public_ipv4='127.0.0.1',
985 private_ipv4=None,
986 public_ipv6=None,
987 allocated_to=request_id,
988 state='ready',
989 state_time=now,
990 created_time=now,
991 updated_time=now,
992 image_id=None,
Paul Belanger174a8272017-03-14 13:20:10 -0400993 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -0800994 data = json.dumps(data)
995 path = self.client.create(path, data,
996 makepath=True,
997 sequence=True)
998 nodeid = path.split("/")[-1]
999 return nodeid
1000
James E. Blair6ab79e02017-01-06 10:10:17 -08001001 def addFailRequest(self, request):
1002 self.fail_requests.add(request['_oid'])
1003
James E. Blairdce6cea2016-12-20 16:45:32 -08001004 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001005 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001006 return
1007 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001008 oid = request['_oid']
1009 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001010
James E. Blair6ab79e02017-01-06 10:10:17 -08001011 if oid in self.fail_requests:
1012 request['state'] = 'failed'
1013 else:
1014 request['state'] = 'fulfilled'
1015 nodes = []
1016 for node in request['node_types']:
1017 nodeid = self.makeNode(oid, node)
1018 nodes.append(nodeid)
1019 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001020
James E. Blaira38c28e2017-01-04 10:33:20 -08001021 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001022 path = self.REQUEST_ROOT + '/' + oid
1023 data = json.dumps(request)
1024 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1025 self.client.set(path, data)
1026
1027
James E. Blair498059b2016-12-20 13:50:13 -08001028class ChrootedKazooFixture(fixtures.Fixture):
1029 def __init__(self):
1030 super(ChrootedKazooFixture, self).__init__()
1031
1032 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1033 if ':' in zk_host:
1034 host, port = zk_host.split(':')
1035 else:
1036 host = zk_host
1037 port = None
1038
1039 self.zookeeper_host = host
1040
1041 if not port:
1042 self.zookeeper_port = 2181
1043 else:
1044 self.zookeeper_port = int(port)
1045
1046 def _setUp(self):
1047 # Make sure the test chroot paths do not conflict
1048 random_bits = ''.join(random.choice(string.ascii_lowercase +
1049 string.ascii_uppercase)
1050 for x in range(8))
1051
1052 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1053 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1054
1055 # Ensure the chroot path exists and clean up any pre-existing znodes.
1056 _tmp_client = kazoo.client.KazooClient(
1057 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1058 _tmp_client.start()
1059
1060 if _tmp_client.exists(self.zookeeper_chroot):
1061 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1062
1063 _tmp_client.ensure_path(self.zookeeper_chroot)
1064 _tmp_client.stop()
1065 _tmp_client.close()
1066
1067 self.addCleanup(self._cleanup)
1068
1069 def _cleanup(self):
1070 '''Remove the chroot path.'''
1071 # Need a non-chroot'ed client to remove the chroot path
1072 _tmp_client = kazoo.client.KazooClient(
1073 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1074 _tmp_client.start()
1075 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1076 _tmp_client.stop()
1077
1078
Joshua Heskethd78b4482015-09-14 16:56:34 -06001079class MySQLSchemaFixture(fixtures.Fixture):
1080 def setUp(self):
1081 super(MySQLSchemaFixture, self).setUp()
1082
1083 random_bits = ''.join(random.choice(string.ascii_lowercase +
1084 string.ascii_uppercase)
1085 for x in range(8))
1086 self.name = '%s_%s' % (random_bits, os.getpid())
1087 self.passwd = uuid.uuid4().hex
1088 db = pymysql.connect(host="localhost",
1089 user="openstack_citest",
1090 passwd="openstack_citest",
1091 db="openstack_citest")
1092 cur = db.cursor()
1093 cur.execute("create database %s" % self.name)
1094 cur.execute(
1095 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1096 (self.name, self.name, self.passwd))
1097 cur.execute("flush privileges")
1098
1099 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1100 self.passwd,
1101 self.name)
1102 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1103 self.addCleanup(self.cleanup)
1104
1105 def cleanup(self):
1106 db = pymysql.connect(host="localhost",
1107 user="openstack_citest",
1108 passwd="openstack_citest",
1109 db="openstack_citest")
1110 cur = db.cursor()
1111 cur.execute("drop database %s" % self.name)
1112 cur.execute("drop user '%s'@'localhost'" % self.name)
1113 cur.execute("flush privileges")
1114
1115
Maru Newby3fe5f852015-01-13 04:22:14 +00001116class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001117 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001118 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001119
James E. Blair1c236df2017-02-01 14:07:24 -08001120 def attachLogs(self, *args):
1121 def reader():
1122 self._log_stream.seek(0)
1123 while True:
1124 x = self._log_stream.read(4096)
1125 if not x:
1126 break
1127 yield x.encode('utf8')
1128 content = testtools.content.content_from_reader(
1129 reader,
1130 testtools.content_type.UTF8_TEXT,
1131 False)
1132 self.addDetail('logging', content)
1133
Clark Boylanb640e052014-04-03 16:41:46 -07001134 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001135 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001136 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1137 try:
1138 test_timeout = int(test_timeout)
1139 except ValueError:
1140 # If timeout value is invalid do not set a timeout.
1141 test_timeout = 0
1142 if test_timeout > 0:
1143 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1144
1145 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1146 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1147 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1148 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1149 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1150 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1151 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1152 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1153 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1154 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001155 self._log_stream = StringIO()
1156 self.addOnException(self.attachLogs)
1157 else:
1158 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001159
James E. Blair1c236df2017-02-01 14:07:24 -08001160 handler = logging.StreamHandler(self._log_stream)
1161 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1162 '%(levelname)-8s %(message)s')
1163 handler.setFormatter(formatter)
1164
1165 logger = logging.getLogger()
1166 logger.setLevel(logging.DEBUG)
1167 logger.addHandler(handler)
1168
1169 # NOTE(notmorgan): Extract logging overrides for specific
1170 # libraries from the OS_LOG_DEFAULTS env and create loggers
1171 # for each. This is used to limit the output during test runs
1172 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001173 log_defaults_from_env = os.environ.get(
1174 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001175 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001176
James E. Blairdce6cea2016-12-20 16:45:32 -08001177 if log_defaults_from_env:
1178 for default in log_defaults_from_env.split(','):
1179 try:
1180 name, level_str = default.split('=', 1)
1181 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001182 logger = logging.getLogger(name)
1183 logger.setLevel(level)
1184 logger.addHandler(handler)
1185 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001186 except ValueError:
1187 # NOTE(notmorgan): Invalid format of the log default,
1188 # skip and don't try and apply a logger for the
1189 # specified module
1190 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001191
Maru Newby3fe5f852015-01-13 04:22:14 +00001192
1193class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001194 """A test case with a functioning Zuul.
1195
1196 The following class variables are used during test setup and can
1197 be overidden by subclasses but are effectively read-only once a
1198 test method starts running:
1199
1200 :cvar str config_file: This points to the main zuul config file
1201 within the fixtures directory. Subclasses may override this
1202 to obtain a different behavior.
1203
1204 :cvar str tenant_config_file: This is the tenant config file
1205 (which specifies from what git repos the configuration should
1206 be loaded). It defaults to the value specified in
1207 `config_file` but can be overidden by subclasses to obtain a
1208 different tenant/project layout while using the standard main
1209 configuration.
1210
1211 The following are instance variables that are useful within test
1212 methods:
1213
1214 :ivar FakeGerritConnection fake_<connection>:
1215 A :py:class:`~tests.base.FakeGerritConnection` will be
1216 instantiated for each connection present in the config file
1217 and stored here. For instance, `fake_gerrit` will hold the
1218 FakeGerritConnection object for a connection named `gerrit`.
1219
1220 :ivar FakeGearmanServer gearman_server: An instance of
1221 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1222 server that all of the Zuul components in this test use to
1223 communicate with each other.
1224
Paul Belanger174a8272017-03-14 13:20:10 -04001225 :ivar RecordingExecutorServer executor_server: An instance of
1226 :py:class:`~tests.base.RecordingExecutorServer` which is the
1227 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001228
1229 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1230 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001231 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001232 list upon completion.
1233
1234 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1235 objects representing completed builds. They are appended to
1236 the list in the order they complete.
1237
1238 """
1239
James E. Blair83005782015-12-11 14:46:03 -08001240 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001241 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001242
1243 def _startMerger(self):
1244 self.merge_server = zuul.merger.server.MergeServer(self.config,
1245 self.connections)
1246 self.merge_server.start()
1247
Maru Newby3fe5f852015-01-13 04:22:14 +00001248 def setUp(self):
1249 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001250
1251 self.setupZK()
1252
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001253 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001254 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001255 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1256 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001257 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001258 tmp_root = tempfile.mkdtemp(
1259 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001260 self.test_root = os.path.join(tmp_root, "zuul-test")
1261 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001262 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001263 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001264 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001265
1266 if os.path.exists(self.test_root):
1267 shutil.rmtree(self.test_root)
1268 os.makedirs(self.test_root)
1269 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001270 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001271
1272 # Make per test copy of Configuration.
1273 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001274 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001275 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001276 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001277 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001278 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001279 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001280
1281 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001282 # TODOv3(jeblair): remove these and replace with new git
1283 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001284 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001285 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001286 self.init_repo("org/project5")
1287 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001288 self.init_repo("org/one-job-project")
1289 self.init_repo("org/nonvoting-project")
1290 self.init_repo("org/templated-project")
1291 self.init_repo("org/layered-project")
1292 self.init_repo("org/node-project")
1293 self.init_repo("org/conflict-project")
1294 self.init_repo("org/noop-project")
1295 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001296 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001297
1298 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001299 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1300 # see: https://github.com/jsocol/pystatsd/issues/61
1301 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001302 os.environ['STATSD_PORT'] = str(self.statsd.port)
1303 self.statsd.start()
1304 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001305 reload_module(statsd)
1306 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001307
1308 self.gearman_server = FakeGearmanServer()
1309
1310 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001311 self.log.info("Gearman server on port %s" %
1312 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001313
James E. Blaire511d2f2016-12-08 15:22:26 -08001314 gerritsource.GerritSource.replication_timeout = 1.5
1315 gerritsource.GerritSource.replication_retry_interval = 0.5
1316 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001317
Joshua Hesketh352264b2015-08-11 23:42:08 +10001318 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001319
Jan Hruban6b71aff2015-10-22 16:58:08 +02001320 self.event_queues = [
1321 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001322 self.sched.trigger_event_queue,
1323 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001324 ]
1325
James E. Blairfef78942016-03-11 16:28:56 -08001326 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001327 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001328
Clark Boylanb640e052014-04-03 16:41:46 -07001329 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001330 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001331 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001332 return FakeURLOpener(self.upstream_root, *args, **kw)
1333
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001334 old_urlopen = urllib.request.urlopen
1335 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001336
James E. Blair3f876d52016-07-22 13:07:14 -07001337 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001338
Paul Belanger174a8272017-03-14 13:20:10 -04001339 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001340 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001341 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001342 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001343 _test_root=self.test_root,
1344 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001345 self.executor_server.start()
1346 self.history = self.executor_server.build_history
1347 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001348
Paul Belanger174a8272017-03-14 13:20:10 -04001349 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001350 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001351 self.merge_client = zuul.merger.client.MergeClient(
1352 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001353 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001354 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001355 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001356
James E. Blair0d5a36e2017-02-21 10:53:44 -05001357 self.fake_nodepool = FakeNodepool(
1358 self.zk_chroot_fixture.zookeeper_host,
1359 self.zk_chroot_fixture.zookeeper_port,
1360 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001361
Paul Belanger174a8272017-03-14 13:20:10 -04001362 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001363 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001364 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001365 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001366
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001367 self.webapp = zuul.webapp.WebApp(
1368 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001369 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001370
1371 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001372 self.webapp.start()
1373 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001374 self.executor_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001375 self.addCleanup(self.shutdown)
1376
James E. Blairb9c0d772017-03-03 14:34:49 -08001377 self.sched.reconfigure(self.config)
1378 self.sched.resume()
1379
James E. Blaire18d4602017-01-05 11:17:28 -08001380 def tearDown(self):
1381 super(ZuulTestCase, self).tearDown()
1382 self.assertFinalState()
1383
James E. Blairfef78942016-03-11 16:28:56 -08001384 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001385 # Set up gerrit related fakes
1386 # Set a changes database so multiple FakeGerrit's can report back to
1387 # a virtual canonical database given by the configured hostname
1388 self.gerrit_changes_dbs = {}
1389
1390 def getGerritConnection(driver, name, config):
1391 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1392 con = FakeGerritConnection(driver, name, config,
1393 changes_db=db,
1394 upstream_root=self.upstream_root)
1395 self.event_queues.append(con.event_queue)
1396 setattr(self, 'fake_' + name, con)
1397 return con
1398
1399 self.useFixture(fixtures.MonkeyPatch(
1400 'zuul.driver.gerrit.GerritDriver.getConnection',
1401 getGerritConnection))
1402
1403 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001404 # TODO(jhesketh): This should come from lib.connections for better
1405 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001406 # Register connections from the config
1407 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001408
Joshua Hesketh352264b2015-08-11 23:42:08 +10001409 def FakeSMTPFactory(*args, **kw):
1410 args = [self.smtp_messages] + list(args)
1411 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001412
Joshua Hesketh352264b2015-08-11 23:42:08 +10001413 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001414
James E. Blaire511d2f2016-12-08 15:22:26 -08001415 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001416 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001417 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001418
James E. Blair83005782015-12-11 14:46:03 -08001419 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001420 # This creates the per-test configuration object. It can be
1421 # overriden by subclasses, but should not need to be since it
1422 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001423 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001424 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001425 if hasattr(self, 'tenant_config_file'):
1426 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001427 git_path = os.path.join(
1428 os.path.dirname(
1429 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1430 'git')
1431 if os.path.exists(git_path):
1432 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001433 project = reponame.replace('_', '/')
1434 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001435 os.path.join(git_path, reponame))
1436
James E. Blair498059b2016-12-20 13:50:13 -08001437 def setupZK(self):
1438 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001439 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001440 self.zk_chroot_fixture.zookeeper_host,
1441 self.zk_chroot_fixture.zookeeper_port,
1442 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001443
James E. Blair96c6bf82016-01-15 16:20:40 -08001444 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001445 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001446
1447 files = {}
1448 for (dirpath, dirnames, filenames) in os.walk(source_path):
1449 for filename in filenames:
1450 test_tree_filepath = os.path.join(dirpath, filename)
1451 common_path = os.path.commonprefix([test_tree_filepath,
1452 source_path])
1453 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1454 with open(test_tree_filepath, 'r') as f:
1455 content = f.read()
1456 files[relative_filepath] = content
1457 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001458 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001459
James E. Blaire18d4602017-01-05 11:17:28 -08001460 def assertNodepoolState(self):
1461 # Make sure that there are no pending requests
1462
1463 requests = self.fake_nodepool.getNodeRequests()
1464 self.assertEqual(len(requests), 0)
1465
1466 nodes = self.fake_nodepool.getNodes()
1467 for node in nodes:
1468 self.assertFalse(node['_lock'], "Node %s is locked" %
1469 (node['_oid'],))
1470
Clark Boylanb640e052014-04-03 16:41:46 -07001471 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001472 # Make sure that git.Repo objects have been garbage collected.
1473 repos = []
1474 gc.collect()
1475 for obj in gc.get_objects():
1476 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001477 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001478 repos.append(obj)
1479 self.assertEqual(len(repos), 0)
1480 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001481 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001482 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001483 for tenant in self.sched.abide.tenants.values():
1484 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001485 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001486 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001487
1488 def shutdown(self):
1489 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001490 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001491 self.merge_server.stop()
1492 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001493 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001494 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001495 self.sched.stop()
1496 self.sched.join()
1497 self.statsd.stop()
1498 self.statsd.join()
1499 self.webapp.stop()
1500 self.webapp.join()
1501 self.rpc.stop()
1502 self.rpc.join()
1503 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001504 self.fake_nodepool.stop()
1505 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001506 threads = threading.enumerate()
1507 if len(threads) > 1:
1508 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001509 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001510
1511 def init_repo(self, project):
1512 parts = project.split('/')
1513 path = os.path.join(self.upstream_root, *parts[:-1])
1514 if not os.path.exists(path):
1515 os.makedirs(path)
1516 path = os.path.join(self.upstream_root, project)
1517 repo = git.Repo.init(path)
1518
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001519 with repo.config_writer() as config_writer:
1520 config_writer.set_value('user', 'email', 'user@example.com')
1521 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001522
Clark Boylanb640e052014-04-03 16:41:46 -07001523 repo.index.commit('initial commit')
1524 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001525
James E. Blair97d902e2014-08-21 13:25:56 -07001526 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001527 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001528 repo.git.clean('-x', '-f', '-d')
1529
James E. Blair97d902e2014-08-21 13:25:56 -07001530 def create_branch(self, project, branch):
1531 path = os.path.join(self.upstream_root, project)
1532 repo = git.Repo.init(path)
1533 fn = os.path.join(path, 'README')
1534
1535 branch_head = repo.create_head(branch)
1536 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001537 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001538 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001539 f.close()
1540 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001541 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001542
James E. Blair97d902e2014-08-21 13:25:56 -07001543 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001544 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001545 repo.git.clean('-x', '-f', '-d')
1546
Sachi King9f16d522016-03-16 12:20:45 +11001547 def create_commit(self, project):
1548 path = os.path.join(self.upstream_root, project)
1549 repo = git.Repo(path)
1550 repo.head.reference = repo.heads['master']
1551 file_name = os.path.join(path, 'README')
1552 with open(file_name, 'a') as f:
1553 f.write('creating fake commit\n')
1554 repo.index.add([file_name])
1555 commit = repo.index.commit('Creating a fake commit')
1556 return commit.hexsha
1557
James E. Blairb8c16472015-05-05 14:55:26 -07001558 def orderedRelease(self):
1559 # Run one build at a time to ensure non-race order:
1560 while len(self.builds):
1561 self.release(self.builds[0])
1562 self.waitUntilSettled()
1563
Clark Boylanb640e052014-04-03 16:41:46 -07001564 def release(self, job):
1565 if isinstance(job, FakeBuild):
1566 job.release()
1567 else:
1568 job.waiting = False
1569 self.log.debug("Queued job %s released" % job.unique)
1570 self.gearman_server.wakeConnections()
1571
1572 def getParameter(self, job, name):
1573 if isinstance(job, FakeBuild):
1574 return job.parameters[name]
1575 else:
1576 parameters = json.loads(job.arguments)
1577 return parameters[name]
1578
Clark Boylanb640e052014-04-03 16:41:46 -07001579 def haveAllBuildsReported(self):
1580 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001581 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001582 return False
1583 # Find out if every build that the worker has completed has been
1584 # reported back to Zuul. If it hasn't then that means a Gearman
1585 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001586 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001587 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001588 if not zbuild:
1589 # It has already been reported
1590 continue
1591 # It hasn't been reported yet.
1592 return False
1593 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001594 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001595 if connection.state == 'GRAB_WAIT':
1596 return False
1597 return True
1598
1599 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001600 builds = self.executor_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001601 for build in builds:
1602 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001603 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001604 for j in conn.related_jobs.values():
1605 if j.unique == build.uuid:
1606 client_job = j
1607 break
1608 if not client_job:
1609 self.log.debug("%s is not known to the gearman client" %
1610 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001611 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001612 if not client_job.handle:
1613 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001614 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001615 server_job = self.gearman_server.jobs.get(client_job.handle)
1616 if not server_job:
1617 self.log.debug("%s is not known to the gearman server" %
1618 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001619 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001620 if not hasattr(server_job, 'waiting'):
1621 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001622 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001623 if server_job.waiting:
1624 continue
James E. Blair17302972016-08-10 16:11:42 -07001625 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001626 self.log.debug("%s has not reported start" % build)
1627 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001628 worker_build = self.executor_server.job_builds.get(
1629 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001630 if worker_build:
1631 if worker_build.isWaiting():
1632 continue
1633 else:
1634 self.log.debug("%s is running" % worker_build)
1635 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001636 else:
James E. Blair962220f2016-08-03 11:22:38 -07001637 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001638 return False
1639 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001640
James E. Blairdce6cea2016-12-20 16:45:32 -08001641 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001642 if self.fake_nodepool.paused:
1643 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001644 if self.sched.nodepool.requests:
1645 return False
1646 return True
1647
Jan Hruban6b71aff2015-10-22 16:58:08 +02001648 def eventQueuesEmpty(self):
1649 for queue in self.event_queues:
1650 yield queue.empty()
1651
1652 def eventQueuesJoin(self):
1653 for queue in self.event_queues:
1654 queue.join()
1655
Clark Boylanb640e052014-04-03 16:41:46 -07001656 def waitUntilSettled(self):
1657 self.log.debug("Waiting until settled...")
1658 start = time.time()
1659 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001660 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001661 self.log.error("Timeout waiting for Zuul to settle")
1662 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001663 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001664 self.log.error(" %s: %s" % (queue, queue.empty()))
1665 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001666 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001667 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001668 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001669 self.log.error("All requests completed: %s" %
1670 (self.areAllNodeRequestsComplete(),))
1671 self.log.error("Merge client jobs: %s" %
1672 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001673 raise Exception("Timeout waiting for Zuul to settle")
1674 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001675
Paul Belanger174a8272017-03-14 13:20:10 -04001676 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001677 # have all build states propogated to zuul?
1678 if self.haveAllBuildsReported():
1679 # Join ensures that the queue is empty _and_ events have been
1680 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001681 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001682 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001683 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001684 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001685 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001686 self.areAllNodeRequestsComplete() and
1687 all(self.eventQueuesEmpty())):
1688 # The queue empty check is placed at the end to
1689 # ensure that if a component adds an event between
1690 # when locked the run handler and checked that the
1691 # components were stable, we don't erroneously
1692 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001693 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001694 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001695 self.log.debug("...settled.")
1696 return
1697 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001698 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001699 self.sched.wake_event.wait(0.1)
1700
1701 def countJobResults(self, jobs, result):
1702 jobs = filter(lambda x: x.result == result, jobs)
1703 return len(jobs)
1704
James E. Blair96c6bf82016-01-15 16:20:40 -08001705 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001706 for job in self.history:
1707 if (job.name == name and
1708 (project is None or
1709 job.parameters['ZUUL_PROJECT'] == project)):
1710 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001711 raise Exception("Unable to find job %s in history" % name)
1712
1713 def assertEmptyQueues(self):
1714 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001715 for tenant in self.sched.abide.tenants.values():
1716 for pipeline in tenant.layout.pipelines.values():
1717 for queue in pipeline.queues:
1718 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001719 print('pipeline %s queue %s contents %s' % (
1720 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001721 self.assertEqual(len(queue.queue), 0,
1722 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001723
1724 def assertReportedStat(self, key, value=None, kind=None):
1725 start = time.time()
1726 while time.time() < (start + 5):
1727 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001728 k, v = stat.split(':')
1729 if key == k:
1730 if value is None and kind is None:
1731 return
1732 elif value:
1733 if value == v:
1734 return
1735 elif kind:
1736 if v.endswith('|' + kind):
1737 return
1738 time.sleep(0.1)
1739
Clark Boylanb640e052014-04-03 16:41:46 -07001740 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001741
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001742 def assertBuilds(self, builds):
1743 """Assert that the running builds are as described.
1744
1745 The list of running builds is examined and must match exactly
1746 the list of builds described by the input.
1747
1748 :arg list builds: A list of dictionaries. Each item in the
1749 list must match the corresponding build in the build
1750 history, and each element of the dictionary must match the
1751 corresponding attribute of the build.
1752
1753 """
James E. Blair3158e282016-08-19 09:34:11 -07001754 try:
1755 self.assertEqual(len(self.builds), len(builds))
1756 for i, d in enumerate(builds):
1757 for k, v in d.items():
1758 self.assertEqual(
1759 getattr(self.builds[i], k), v,
1760 "Element %i in builds does not match" % (i,))
1761 except Exception:
1762 for build in self.builds:
1763 self.log.error("Running build: %s" % build)
1764 else:
1765 self.log.error("No running builds")
1766 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001767
James E. Blairb536ecc2016-08-31 10:11:42 -07001768 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001769 """Assert that the completed builds are as described.
1770
1771 The list of completed builds is examined and must match
1772 exactly the list of builds described by the input.
1773
1774 :arg list history: A list of dictionaries. Each item in the
1775 list must match the corresponding build in the build
1776 history, and each element of the dictionary must match the
1777 corresponding attribute of the build.
1778
James E. Blairb536ecc2016-08-31 10:11:42 -07001779 :arg bool ordered: If true, the history must match the order
1780 supplied, if false, the builds are permitted to have
1781 arrived in any order.
1782
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001783 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001784 def matches(history_item, item):
1785 for k, v in item.items():
1786 if getattr(history_item, k) != v:
1787 return False
1788 return True
James E. Blair3158e282016-08-19 09:34:11 -07001789 try:
1790 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001791 if ordered:
1792 for i, d in enumerate(history):
1793 if not matches(self.history[i], d):
1794 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001795 "Element %i in history does not match %s" %
1796 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001797 else:
1798 unseen = self.history[:]
1799 for i, d in enumerate(history):
1800 found = False
1801 for unseen_item in unseen:
1802 if matches(unseen_item, d):
1803 found = True
1804 unseen.remove(unseen_item)
1805 break
1806 if not found:
1807 raise Exception("No match found for element %i "
1808 "in history" % (i,))
1809 if unseen:
1810 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001811 except Exception:
1812 for build in self.history:
1813 self.log.error("Completed build: %s" % build)
1814 else:
1815 self.log.error("No completed builds")
1816 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001817
James E. Blair6ac368c2016-12-22 18:07:20 -08001818 def printHistory(self):
1819 """Log the build history.
1820
1821 This can be useful during tests to summarize what jobs have
1822 completed.
1823
1824 """
1825 self.log.debug("Build history:")
1826 for build in self.history:
1827 self.log.debug(build)
1828
James E. Blair59fdbac2015-12-07 17:08:06 -08001829 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001830 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1831
1832 def updateConfigLayout(self, path):
1833 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001834 if not os.path.exists(root):
1835 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001836 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1837 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001838- tenant:
1839 name: openstack
1840 source:
1841 gerrit:
1842 config-repos:
1843 - %s
1844 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001845 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001846 self.config.set('zuul', 'tenant_config',
1847 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001848
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001849 def addCommitToRepo(self, project, message, files,
1850 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001851 path = os.path.join(self.upstream_root, project)
1852 repo = git.Repo(path)
1853 repo.head.reference = branch
1854 zuul.merger.merger.reset_repo_to_head(repo)
1855 for fn, content in files.items():
1856 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001857 try:
1858 os.makedirs(os.path.dirname(fn))
1859 except OSError:
1860 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001861 with open(fn, 'w') as f:
1862 f.write(content)
1863 repo.index.add([fn])
1864 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001865 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001866 repo.heads[branch].commit = commit
1867 repo.head.reference = branch
1868 repo.git.clean('-x', '-f', '-d')
1869 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001870 if tag:
1871 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001872 return before
1873
1874 def commitLayoutUpdate(self, orig_name, source_name):
1875 source_path = os.path.join(self.test_root, 'upstream',
1876 source_name, 'zuul.yaml')
1877 with open(source_path, 'r') as nt:
1878 before = self.addCommitToRepo(
1879 orig_name, 'Pulling content from %s' % source_name,
1880 {'zuul.yaml': nt.read()})
1881 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001882
James E. Blair7fc8daa2016-08-08 15:37:15 -07001883 def addEvent(self, connection, event):
1884 """Inject a Fake (Gerrit) event.
1885
1886 This method accepts a JSON-encoded event and simulates Zuul
1887 having received it from Gerrit. It could (and should)
1888 eventually apply to any connection type, but is currently only
1889 used with Gerrit connections. The name of the connection is
1890 used to look up the corresponding server, and the event is
1891 simulated as having been received by all Zuul connections
1892 attached to that server. So if two Gerrit connections in Zuul
1893 are connected to the same Gerrit server, and you invoke this
1894 method specifying the name of one of them, the event will be
1895 received by both.
1896
1897 .. note::
1898
1899 "self.fake_gerrit.addEvent" calls should be migrated to
1900 this method.
1901
1902 :arg str connection: The name of the connection corresponding
1903 to the gerrit server.
1904 :arg str event: The JSON-encoded event.
1905
1906 """
1907 specified_conn = self.connections.connections[connection]
1908 for conn in self.connections.connections.values():
1909 if (isinstance(conn, specified_conn.__class__) and
1910 specified_conn.server == conn.server):
1911 conn.addEvent(event)
1912
James E. Blair3f876d52016-07-22 13:07:14 -07001913
1914class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04001915 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001916 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11001917
Joshua Heskethd78b4482015-09-14 16:56:34 -06001918
1919class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08001920 def setup_config(self):
1921 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06001922 for section_name in self.config.sections():
1923 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1924 section_name, re.I)
1925 if not con_match:
1926 continue
1927
1928 if self.config.get(section_name, 'driver') == 'sql':
1929 f = MySQLSchemaFixture()
1930 self.useFixture(f)
1931 if (self.config.get(section_name, 'dburi') ==
1932 '$MYSQL_FIXTURE_DBURI$'):
1933 self.config.set(section_name, 'dburi', f.dburi)