blob: 826a8489d4fdf907ad54fc1fd80dabc2fc002f99 [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):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700773 def doMergeChanges(self, items):
774 # Get a merger in order to update the repos involved in this job.
775 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
776 if not commit: # merge conflict
777 self.recordResult('MERGER_FAILURE')
778 return commit
779
780 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400781 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400782 self.executor_server.lock.acquire()
783 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700784 BuildHistory(name=build.name, result=result, changes=build.changes,
785 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800786 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700787 pipeline=build.parameters['ZUUL_PIPELINE'])
788 )
Paul Belanger174a8272017-03-14 13:20:10 -0400789 self.executor_server.running_builds.remove(build)
790 del self.executor_server.job_builds[self.job.unique]
791 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700792
793 def runPlaybooks(self, args):
794 build = self.executor_server.job_builds[self.job.unique]
795 build.jobdir = self.jobdir
796
797 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
798 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800799 return result
800
Monty Taylore6562aa2017-02-20 07:37:39 -0500801 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400802 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800803
Paul Belanger174a8272017-03-14 13:20:10 -0400804 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600805 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500806 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800807 else:
808 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700809 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800810
James E. Blairad8dca02017-02-21 11:48:32 -0500811 def getHostList(self, args):
812 self.log.debug("hostlist")
813 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400814 for host in hosts:
815 host['host_vars']['ansible_connection'] = 'local'
816
817 hosts.append(dict(
818 name='localhost',
819 host_vars=dict(ansible_connection='local'),
820 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500821 return hosts
822
James E. Blairf5dbd002015-12-23 15:26:17 -0800823
Clark Boylanb640e052014-04-03 16:41:46 -0700824class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700825 """A Gearman server for use in tests.
826
827 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
828 added to the queue but will not be distributed to workers
829 until released. This attribute may be changed at any time and
830 will take effect for subsequently enqueued jobs, but
831 previously held jobs will still need to be explicitly
832 released.
833
834 """
835
Clark Boylanb640e052014-04-03 16:41:46 -0700836 def __init__(self):
837 self.hold_jobs_in_queue = False
838 super(FakeGearmanServer, self).__init__(0)
839
840 def getJobForConnection(self, connection, peek=False):
841 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
842 for job in queue:
843 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400844 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700845 job.waiting = self.hold_jobs_in_queue
846 else:
847 job.waiting = False
848 if job.waiting:
849 continue
850 if job.name in connection.functions:
851 if not peek:
852 queue.remove(job)
853 connection.related_jobs[job.handle] = job
854 job.worker_connection = connection
855 job.running = True
856 return job
857 return None
858
859 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700860 """Release a held job.
861
862 :arg str regex: A regular expression which, if supplied, will
863 cause only jobs with matching names to be released. If
864 not supplied, all jobs will be released.
865 """
Clark Boylanb640e052014-04-03 16:41:46 -0700866 released = False
867 qlen = (len(self.high_queue) + len(self.normal_queue) +
868 len(self.low_queue))
869 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
870 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400871 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700872 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500873 parameters = json.loads(job.arguments)
874 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700875 self.log.debug("releasing queued job %s" %
876 job.unique)
877 job.waiting = False
878 released = True
879 else:
880 self.log.debug("not releasing queued job %s" %
881 job.unique)
882 if released:
883 self.wakeConnections()
884 qlen = (len(self.high_queue) + len(self.normal_queue) +
885 len(self.low_queue))
886 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
887
888
889class FakeSMTP(object):
890 log = logging.getLogger('zuul.FakeSMTP')
891
892 def __init__(self, messages, server, port):
893 self.server = server
894 self.port = port
895 self.messages = messages
896
897 def sendmail(self, from_email, to_email, msg):
898 self.log.info("Sending email from %s, to %s, with msg %s" % (
899 from_email, to_email, msg))
900
901 headers = msg.split('\n\n', 1)[0]
902 body = msg.split('\n\n', 1)[1]
903
904 self.messages.append(dict(
905 from_email=from_email,
906 to_email=to_email,
907 msg=msg,
908 headers=headers,
909 body=body,
910 ))
911
912 return True
913
914 def quit(self):
915 return True
916
917
James E. Blairdce6cea2016-12-20 16:45:32 -0800918class FakeNodepool(object):
919 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800920 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800921
922 log = logging.getLogger("zuul.test.FakeNodepool")
923
924 def __init__(self, host, port, chroot):
925 self.client = kazoo.client.KazooClient(
926 hosts='%s:%s%s' % (host, port, chroot))
927 self.client.start()
928 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800929 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800930 self.thread = threading.Thread(target=self.run)
931 self.thread.daemon = True
932 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800933 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800934
935 def stop(self):
936 self._running = False
937 self.thread.join()
938 self.client.stop()
939 self.client.close()
940
941 def run(self):
942 while self._running:
943 self._run()
944 time.sleep(0.1)
945
946 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800947 if self.paused:
948 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800949 for req in self.getNodeRequests():
950 self.fulfillRequest(req)
951
952 def getNodeRequests(self):
953 try:
954 reqids = self.client.get_children(self.REQUEST_ROOT)
955 except kazoo.exceptions.NoNodeError:
956 return []
957 reqs = []
958 for oid in sorted(reqids):
959 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800960 try:
961 data, stat = self.client.get(path)
962 data = json.loads(data)
963 data['_oid'] = oid
964 reqs.append(data)
965 except kazoo.exceptions.NoNodeError:
966 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800967 return reqs
968
James E. Blaire18d4602017-01-05 11:17:28 -0800969 def getNodes(self):
970 try:
971 nodeids = self.client.get_children(self.NODE_ROOT)
972 except kazoo.exceptions.NoNodeError:
973 return []
974 nodes = []
975 for oid in sorted(nodeids):
976 path = self.NODE_ROOT + '/' + oid
977 data, stat = self.client.get(path)
978 data = json.loads(data)
979 data['_oid'] = oid
980 try:
981 lockfiles = self.client.get_children(path + '/lock')
982 except kazoo.exceptions.NoNodeError:
983 lockfiles = []
984 if lockfiles:
985 data['_lock'] = True
986 else:
987 data['_lock'] = False
988 nodes.append(data)
989 return nodes
990
James E. Blaira38c28e2017-01-04 10:33:20 -0800991 def makeNode(self, request_id, node_type):
992 now = time.time()
993 path = '/nodepool/nodes/'
994 data = dict(type=node_type,
995 provider='test-provider',
996 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -0400997 az='test-az',
James E. Blaira38c28e2017-01-04 10:33:20 -0800998 public_ipv4='127.0.0.1',
999 private_ipv4=None,
1000 public_ipv6=None,
1001 allocated_to=request_id,
1002 state='ready',
1003 state_time=now,
1004 created_time=now,
1005 updated_time=now,
1006 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001007 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001008 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001009 data = json.dumps(data)
1010 path = self.client.create(path, data,
1011 makepath=True,
1012 sequence=True)
1013 nodeid = path.split("/")[-1]
1014 return nodeid
1015
James E. Blair6ab79e02017-01-06 10:10:17 -08001016 def addFailRequest(self, request):
1017 self.fail_requests.add(request['_oid'])
1018
James E. Blairdce6cea2016-12-20 16:45:32 -08001019 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001020 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001021 return
1022 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001023 oid = request['_oid']
1024 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001025
James E. Blair6ab79e02017-01-06 10:10:17 -08001026 if oid in self.fail_requests:
1027 request['state'] = 'failed'
1028 else:
1029 request['state'] = 'fulfilled'
1030 nodes = []
1031 for node in request['node_types']:
1032 nodeid = self.makeNode(oid, node)
1033 nodes.append(nodeid)
1034 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001035
James E. Blaira38c28e2017-01-04 10:33:20 -08001036 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001037 path = self.REQUEST_ROOT + '/' + oid
1038 data = json.dumps(request)
1039 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1040 self.client.set(path, data)
1041
1042
James E. Blair498059b2016-12-20 13:50:13 -08001043class ChrootedKazooFixture(fixtures.Fixture):
1044 def __init__(self):
1045 super(ChrootedKazooFixture, self).__init__()
1046
1047 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1048 if ':' in zk_host:
1049 host, port = zk_host.split(':')
1050 else:
1051 host = zk_host
1052 port = None
1053
1054 self.zookeeper_host = host
1055
1056 if not port:
1057 self.zookeeper_port = 2181
1058 else:
1059 self.zookeeper_port = int(port)
1060
1061 def _setUp(self):
1062 # Make sure the test chroot paths do not conflict
1063 random_bits = ''.join(random.choice(string.ascii_lowercase +
1064 string.ascii_uppercase)
1065 for x in range(8))
1066
1067 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1068 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1069
1070 # Ensure the chroot path exists and clean up any pre-existing znodes.
1071 _tmp_client = kazoo.client.KazooClient(
1072 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1073 _tmp_client.start()
1074
1075 if _tmp_client.exists(self.zookeeper_chroot):
1076 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1077
1078 _tmp_client.ensure_path(self.zookeeper_chroot)
1079 _tmp_client.stop()
1080 _tmp_client.close()
1081
1082 self.addCleanup(self._cleanup)
1083
1084 def _cleanup(self):
1085 '''Remove the chroot path.'''
1086 # Need a non-chroot'ed client to remove the chroot path
1087 _tmp_client = kazoo.client.KazooClient(
1088 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1089 _tmp_client.start()
1090 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1091 _tmp_client.stop()
1092
1093
Joshua Heskethd78b4482015-09-14 16:56:34 -06001094class MySQLSchemaFixture(fixtures.Fixture):
1095 def setUp(self):
1096 super(MySQLSchemaFixture, self).setUp()
1097
1098 random_bits = ''.join(random.choice(string.ascii_lowercase +
1099 string.ascii_uppercase)
1100 for x in range(8))
1101 self.name = '%s_%s' % (random_bits, os.getpid())
1102 self.passwd = uuid.uuid4().hex
1103 db = pymysql.connect(host="localhost",
1104 user="openstack_citest",
1105 passwd="openstack_citest",
1106 db="openstack_citest")
1107 cur = db.cursor()
1108 cur.execute("create database %s" % self.name)
1109 cur.execute(
1110 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1111 (self.name, self.name, self.passwd))
1112 cur.execute("flush privileges")
1113
1114 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1115 self.passwd,
1116 self.name)
1117 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1118 self.addCleanup(self.cleanup)
1119
1120 def cleanup(self):
1121 db = pymysql.connect(host="localhost",
1122 user="openstack_citest",
1123 passwd="openstack_citest",
1124 db="openstack_citest")
1125 cur = db.cursor()
1126 cur.execute("drop database %s" % self.name)
1127 cur.execute("drop user '%s'@'localhost'" % self.name)
1128 cur.execute("flush privileges")
1129
1130
Maru Newby3fe5f852015-01-13 04:22:14 +00001131class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001132 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001133 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001134
James E. Blair1c236df2017-02-01 14:07:24 -08001135 def attachLogs(self, *args):
1136 def reader():
1137 self._log_stream.seek(0)
1138 while True:
1139 x = self._log_stream.read(4096)
1140 if not x:
1141 break
1142 yield x.encode('utf8')
1143 content = testtools.content.content_from_reader(
1144 reader,
1145 testtools.content_type.UTF8_TEXT,
1146 False)
1147 self.addDetail('logging', content)
1148
Clark Boylanb640e052014-04-03 16:41:46 -07001149 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001150 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001151 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1152 try:
1153 test_timeout = int(test_timeout)
1154 except ValueError:
1155 # If timeout value is invalid do not set a timeout.
1156 test_timeout = 0
1157 if test_timeout > 0:
1158 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1159
1160 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1161 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1162 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1163 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1164 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1165 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1166 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1167 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1168 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1169 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001170 self._log_stream = StringIO()
1171 self.addOnException(self.attachLogs)
1172 else:
1173 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001174
James E. Blair1c236df2017-02-01 14:07:24 -08001175 handler = logging.StreamHandler(self._log_stream)
1176 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1177 '%(levelname)-8s %(message)s')
1178 handler.setFormatter(formatter)
1179
1180 logger = logging.getLogger()
1181 logger.setLevel(logging.DEBUG)
1182 logger.addHandler(handler)
1183
1184 # NOTE(notmorgan): Extract logging overrides for specific
1185 # libraries from the OS_LOG_DEFAULTS env and create loggers
1186 # for each. This is used to limit the output during test runs
1187 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001188 log_defaults_from_env = os.environ.get(
1189 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001190 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001191
James E. Blairdce6cea2016-12-20 16:45:32 -08001192 if log_defaults_from_env:
1193 for default in log_defaults_from_env.split(','):
1194 try:
1195 name, level_str = default.split('=', 1)
1196 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001197 logger = logging.getLogger(name)
1198 logger.setLevel(level)
1199 logger.addHandler(handler)
1200 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001201 except ValueError:
1202 # NOTE(notmorgan): Invalid format of the log default,
1203 # skip and don't try and apply a logger for the
1204 # specified module
1205 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001206
Maru Newby3fe5f852015-01-13 04:22:14 +00001207
1208class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001209 """A test case with a functioning Zuul.
1210
1211 The following class variables are used during test setup and can
1212 be overidden by subclasses but are effectively read-only once a
1213 test method starts running:
1214
1215 :cvar str config_file: This points to the main zuul config file
1216 within the fixtures directory. Subclasses may override this
1217 to obtain a different behavior.
1218
1219 :cvar str tenant_config_file: This is the tenant config file
1220 (which specifies from what git repos the configuration should
1221 be loaded). It defaults to the value specified in
1222 `config_file` but can be overidden by subclasses to obtain a
1223 different tenant/project layout while using the standard main
1224 configuration.
1225
1226 The following are instance variables that are useful within test
1227 methods:
1228
1229 :ivar FakeGerritConnection fake_<connection>:
1230 A :py:class:`~tests.base.FakeGerritConnection` will be
1231 instantiated for each connection present in the config file
1232 and stored here. For instance, `fake_gerrit` will hold the
1233 FakeGerritConnection object for a connection named `gerrit`.
1234
1235 :ivar FakeGearmanServer gearman_server: An instance of
1236 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1237 server that all of the Zuul components in this test use to
1238 communicate with each other.
1239
Paul Belanger174a8272017-03-14 13:20:10 -04001240 :ivar RecordingExecutorServer executor_server: An instance of
1241 :py:class:`~tests.base.RecordingExecutorServer` which is the
1242 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001243
1244 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1245 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001246 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001247 list upon completion.
1248
1249 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1250 objects representing completed builds. They are appended to
1251 the list in the order they complete.
1252
1253 """
1254
James E. Blair83005782015-12-11 14:46:03 -08001255 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001257
1258 def _startMerger(self):
1259 self.merge_server = zuul.merger.server.MergeServer(self.config,
1260 self.connections)
1261 self.merge_server.start()
1262
Maru Newby3fe5f852015-01-13 04:22:14 +00001263 def setUp(self):
1264 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001265
1266 self.setupZK()
1267
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001268 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001269 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001270 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1271 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001272 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001273 tmp_root = tempfile.mkdtemp(
1274 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001275 self.test_root = os.path.join(tmp_root, "zuul-test")
1276 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001277 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001278 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001279 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001280
1281 if os.path.exists(self.test_root):
1282 shutil.rmtree(self.test_root)
1283 os.makedirs(self.test_root)
1284 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001285 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001286
1287 # Make per test copy of Configuration.
1288 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001289 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001290 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001291 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001292 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001293 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001294 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001295
1296 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001297 # TODOv3(jeblair): remove these and replace with new git
1298 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001299 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001300 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001301 self.init_repo("org/project5")
1302 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001303 self.init_repo("org/one-job-project")
1304 self.init_repo("org/nonvoting-project")
1305 self.init_repo("org/templated-project")
1306 self.init_repo("org/layered-project")
1307 self.init_repo("org/node-project")
1308 self.init_repo("org/conflict-project")
1309 self.init_repo("org/noop-project")
1310 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001311 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001312
1313 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001314 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1315 # see: https://github.com/jsocol/pystatsd/issues/61
1316 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001317 os.environ['STATSD_PORT'] = str(self.statsd.port)
1318 self.statsd.start()
1319 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001320 reload_module(statsd)
1321 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001322
1323 self.gearman_server = FakeGearmanServer()
1324
1325 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001326 self.log.info("Gearman server on port %s" %
1327 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001328
James E. Blaire511d2f2016-12-08 15:22:26 -08001329 gerritsource.GerritSource.replication_timeout = 1.5
1330 gerritsource.GerritSource.replication_retry_interval = 0.5
1331 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001332
Joshua Hesketh352264b2015-08-11 23:42:08 +10001333 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001334
Jan Hruban6b71aff2015-10-22 16:58:08 +02001335 self.event_queues = [
1336 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001337 self.sched.trigger_event_queue,
1338 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001339 ]
1340
James E. Blairfef78942016-03-11 16:28:56 -08001341 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001342 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001343
Clark Boylanb640e052014-04-03 16:41:46 -07001344 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001345 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001346 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001347 return FakeURLOpener(self.upstream_root, *args, **kw)
1348
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001349 old_urlopen = urllib.request.urlopen
1350 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001351
James E. Blair3f876d52016-07-22 13:07:14 -07001352 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001353
Paul Belanger174a8272017-03-14 13:20:10 -04001354 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001355 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001356 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001357 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001358 _test_root=self.test_root,
1359 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001360 self.executor_server.start()
1361 self.history = self.executor_server.build_history
1362 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001363
Paul Belanger174a8272017-03-14 13:20:10 -04001364 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001365 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001366 self.merge_client = zuul.merger.client.MergeClient(
1367 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001368 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001369 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001370 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001371
James E. Blair0d5a36e2017-02-21 10:53:44 -05001372 self.fake_nodepool = FakeNodepool(
1373 self.zk_chroot_fixture.zookeeper_host,
1374 self.zk_chroot_fixture.zookeeper_port,
1375 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001376
Paul Belanger174a8272017-03-14 13:20:10 -04001377 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001378 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001379 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001380 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001381
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001382 self.webapp = zuul.webapp.WebApp(
1383 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001384 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001385
1386 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001387 self.webapp.start()
1388 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001389 self.executor_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001390 self.addCleanup(self.shutdown)
1391
James E. Blairb9c0d772017-03-03 14:34:49 -08001392 self.sched.reconfigure(self.config)
1393 self.sched.resume()
1394
James E. Blaire18d4602017-01-05 11:17:28 -08001395 def tearDown(self):
1396 super(ZuulTestCase, self).tearDown()
1397 self.assertFinalState()
1398
James E. Blairfef78942016-03-11 16:28:56 -08001399 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001400 # Set up gerrit related fakes
1401 # Set a changes database so multiple FakeGerrit's can report back to
1402 # a virtual canonical database given by the configured hostname
1403 self.gerrit_changes_dbs = {}
1404
1405 def getGerritConnection(driver, name, config):
1406 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1407 con = FakeGerritConnection(driver, name, config,
1408 changes_db=db,
1409 upstream_root=self.upstream_root)
1410 self.event_queues.append(con.event_queue)
1411 setattr(self, 'fake_' + name, con)
1412 return con
1413
1414 self.useFixture(fixtures.MonkeyPatch(
1415 'zuul.driver.gerrit.GerritDriver.getConnection',
1416 getGerritConnection))
1417
1418 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001419 # TODO(jhesketh): This should come from lib.connections for better
1420 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001421 # Register connections from the config
1422 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001423
Joshua Hesketh352264b2015-08-11 23:42:08 +10001424 def FakeSMTPFactory(*args, **kw):
1425 args = [self.smtp_messages] + list(args)
1426 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001427
Joshua Hesketh352264b2015-08-11 23:42:08 +10001428 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001429
James E. Blaire511d2f2016-12-08 15:22:26 -08001430 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001431 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001432 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001433
James E. Blair83005782015-12-11 14:46:03 -08001434 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001435 # This creates the per-test configuration object. It can be
1436 # overriden by subclasses, but should not need to be since it
1437 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001438 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001439 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001440 if hasattr(self, 'tenant_config_file'):
1441 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001442 git_path = os.path.join(
1443 os.path.dirname(
1444 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1445 'git')
1446 if os.path.exists(git_path):
1447 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001448 project = reponame.replace('_', '/')
1449 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001450 os.path.join(git_path, reponame))
1451
James E. Blair498059b2016-12-20 13:50:13 -08001452 def setupZK(self):
1453 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001454 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001455 self.zk_chroot_fixture.zookeeper_host,
1456 self.zk_chroot_fixture.zookeeper_port,
1457 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001458
James E. Blair96c6bf82016-01-15 16:20:40 -08001459 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001460 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001461
1462 files = {}
1463 for (dirpath, dirnames, filenames) in os.walk(source_path):
1464 for filename in filenames:
1465 test_tree_filepath = os.path.join(dirpath, filename)
1466 common_path = os.path.commonprefix([test_tree_filepath,
1467 source_path])
1468 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1469 with open(test_tree_filepath, 'r') as f:
1470 content = f.read()
1471 files[relative_filepath] = content
1472 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001473 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001474
James E. Blaire18d4602017-01-05 11:17:28 -08001475 def assertNodepoolState(self):
1476 # Make sure that there are no pending requests
1477
1478 requests = self.fake_nodepool.getNodeRequests()
1479 self.assertEqual(len(requests), 0)
1480
1481 nodes = self.fake_nodepool.getNodes()
1482 for node in nodes:
1483 self.assertFalse(node['_lock'], "Node %s is locked" %
1484 (node['_oid'],))
1485
Clark Boylanb640e052014-04-03 16:41:46 -07001486 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001487 # Make sure that git.Repo objects have been garbage collected.
1488 repos = []
1489 gc.collect()
1490 for obj in gc.get_objects():
1491 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001492 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001493 repos.append(obj)
1494 self.assertEqual(len(repos), 0)
1495 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001496 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001497 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001498 for tenant in self.sched.abide.tenants.values():
1499 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001500 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001501 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001502
1503 def shutdown(self):
1504 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001505 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001506 self.merge_server.stop()
1507 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001508 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001509 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001510 self.sched.stop()
1511 self.sched.join()
1512 self.statsd.stop()
1513 self.statsd.join()
1514 self.webapp.stop()
1515 self.webapp.join()
1516 self.rpc.stop()
1517 self.rpc.join()
1518 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001519 self.fake_nodepool.stop()
1520 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001521 threads = threading.enumerate()
1522 if len(threads) > 1:
1523 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001524 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001525
1526 def init_repo(self, project):
1527 parts = project.split('/')
1528 path = os.path.join(self.upstream_root, *parts[:-1])
1529 if not os.path.exists(path):
1530 os.makedirs(path)
1531 path = os.path.join(self.upstream_root, project)
1532 repo = git.Repo.init(path)
1533
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001534 with repo.config_writer() as config_writer:
1535 config_writer.set_value('user', 'email', 'user@example.com')
1536 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001537
Clark Boylanb640e052014-04-03 16:41:46 -07001538 repo.index.commit('initial commit')
1539 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001540
James E. Blair97d902e2014-08-21 13:25:56 -07001541 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001542 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001543 repo.git.clean('-x', '-f', '-d')
1544
James E. Blair97d902e2014-08-21 13:25:56 -07001545 def create_branch(self, project, branch):
1546 path = os.path.join(self.upstream_root, project)
1547 repo = git.Repo.init(path)
1548 fn = os.path.join(path, 'README')
1549
1550 branch_head = repo.create_head(branch)
1551 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001552 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001553 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001554 f.close()
1555 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001556 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001557
James E. Blair97d902e2014-08-21 13:25:56 -07001558 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001559 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001560 repo.git.clean('-x', '-f', '-d')
1561
Sachi King9f16d522016-03-16 12:20:45 +11001562 def create_commit(self, project):
1563 path = os.path.join(self.upstream_root, project)
1564 repo = git.Repo(path)
1565 repo.head.reference = repo.heads['master']
1566 file_name = os.path.join(path, 'README')
1567 with open(file_name, 'a') as f:
1568 f.write('creating fake commit\n')
1569 repo.index.add([file_name])
1570 commit = repo.index.commit('Creating a fake commit')
1571 return commit.hexsha
1572
James E. Blairb8c16472015-05-05 14:55:26 -07001573 def orderedRelease(self):
1574 # Run one build at a time to ensure non-race order:
1575 while len(self.builds):
1576 self.release(self.builds[0])
1577 self.waitUntilSettled()
1578
Clark Boylanb640e052014-04-03 16:41:46 -07001579 def release(self, job):
1580 if isinstance(job, FakeBuild):
1581 job.release()
1582 else:
1583 job.waiting = False
1584 self.log.debug("Queued job %s released" % job.unique)
1585 self.gearman_server.wakeConnections()
1586
1587 def getParameter(self, job, name):
1588 if isinstance(job, FakeBuild):
1589 return job.parameters[name]
1590 else:
1591 parameters = json.loads(job.arguments)
1592 return parameters[name]
1593
Clark Boylanb640e052014-04-03 16:41:46 -07001594 def haveAllBuildsReported(self):
1595 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001596 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001597 return False
1598 # Find out if every build that the worker has completed has been
1599 # reported back to Zuul. If it hasn't then that means a Gearman
1600 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001601 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001602 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001603 if not zbuild:
1604 # It has already been reported
1605 continue
1606 # It hasn't been reported yet.
1607 return False
1608 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001609 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001610 if connection.state == 'GRAB_WAIT':
1611 return False
1612 return True
1613
1614 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001615 builds = self.executor_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001616 for build in builds:
1617 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001618 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001619 for j in conn.related_jobs.values():
1620 if j.unique == build.uuid:
1621 client_job = j
1622 break
1623 if not client_job:
1624 self.log.debug("%s is not known to the gearman client" %
1625 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001626 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001627 if not client_job.handle:
1628 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001629 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001630 server_job = self.gearman_server.jobs.get(client_job.handle)
1631 if not server_job:
1632 self.log.debug("%s is not known to the gearman server" %
1633 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001634 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001635 if not hasattr(server_job, 'waiting'):
1636 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001637 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001638 if server_job.waiting:
1639 continue
James E. Blair17302972016-08-10 16:11:42 -07001640 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001641 self.log.debug("%s has not reported start" % build)
1642 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001643 worker_build = self.executor_server.job_builds.get(
1644 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001645 if worker_build:
1646 if worker_build.isWaiting():
1647 continue
1648 else:
1649 self.log.debug("%s is running" % worker_build)
1650 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001651 else:
James E. Blair962220f2016-08-03 11:22:38 -07001652 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001653 return False
1654 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001655
James E. Blairdce6cea2016-12-20 16:45:32 -08001656 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001657 if self.fake_nodepool.paused:
1658 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001659 if self.sched.nodepool.requests:
1660 return False
1661 return True
1662
Jan Hruban6b71aff2015-10-22 16:58:08 +02001663 def eventQueuesEmpty(self):
1664 for queue in self.event_queues:
1665 yield queue.empty()
1666
1667 def eventQueuesJoin(self):
1668 for queue in self.event_queues:
1669 queue.join()
1670
Clark Boylanb640e052014-04-03 16:41:46 -07001671 def waitUntilSettled(self):
1672 self.log.debug("Waiting until settled...")
1673 start = time.time()
1674 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001675 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001676 self.log.error("Timeout waiting for Zuul to settle")
1677 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001678 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001679 self.log.error(" %s: %s" % (queue, queue.empty()))
1680 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001681 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001682 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001683 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001684 self.log.error("All requests completed: %s" %
1685 (self.areAllNodeRequestsComplete(),))
1686 self.log.error("Merge client jobs: %s" %
1687 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001688 raise Exception("Timeout waiting for Zuul to settle")
1689 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001690
Paul Belanger174a8272017-03-14 13:20:10 -04001691 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001692 # have all build states propogated to zuul?
1693 if self.haveAllBuildsReported():
1694 # Join ensures that the queue is empty _and_ events have been
1695 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001696 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001697 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001698 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001699 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001700 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001701 self.areAllNodeRequestsComplete() and
1702 all(self.eventQueuesEmpty())):
1703 # The queue empty check is placed at the end to
1704 # ensure that if a component adds an event between
1705 # when locked the run handler and checked that the
1706 # components were stable, we don't erroneously
1707 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001708 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001709 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001710 self.log.debug("...settled.")
1711 return
1712 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001713 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001714 self.sched.wake_event.wait(0.1)
1715
1716 def countJobResults(self, jobs, result):
1717 jobs = filter(lambda x: x.result == result, jobs)
1718 return len(jobs)
1719
James E. Blair96c6bf82016-01-15 16:20:40 -08001720 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001721 for job in self.history:
1722 if (job.name == name and
1723 (project is None or
1724 job.parameters['ZUUL_PROJECT'] == project)):
1725 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001726 raise Exception("Unable to find job %s in history" % name)
1727
1728 def assertEmptyQueues(self):
1729 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001730 for tenant in self.sched.abide.tenants.values():
1731 for pipeline in tenant.layout.pipelines.values():
1732 for queue in pipeline.queues:
1733 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001734 print('pipeline %s queue %s contents %s' % (
1735 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001736 self.assertEqual(len(queue.queue), 0,
1737 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001738
1739 def assertReportedStat(self, key, value=None, kind=None):
1740 start = time.time()
1741 while time.time() < (start + 5):
1742 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001743 k, v = stat.split(':')
1744 if key == k:
1745 if value is None and kind is None:
1746 return
1747 elif value:
1748 if value == v:
1749 return
1750 elif kind:
1751 if v.endswith('|' + kind):
1752 return
1753 time.sleep(0.1)
1754
Clark Boylanb640e052014-04-03 16:41:46 -07001755 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001756
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001757 def assertBuilds(self, builds):
1758 """Assert that the running builds are as described.
1759
1760 The list of running builds is examined and must match exactly
1761 the list of builds described by the input.
1762
1763 :arg list builds: A list of dictionaries. Each item in the
1764 list must match the corresponding build in the build
1765 history, and each element of the dictionary must match the
1766 corresponding attribute of the build.
1767
1768 """
James E. Blair3158e282016-08-19 09:34:11 -07001769 try:
1770 self.assertEqual(len(self.builds), len(builds))
1771 for i, d in enumerate(builds):
1772 for k, v in d.items():
1773 self.assertEqual(
1774 getattr(self.builds[i], k), v,
1775 "Element %i in builds does not match" % (i,))
1776 except Exception:
1777 for build in self.builds:
1778 self.log.error("Running build: %s" % build)
1779 else:
1780 self.log.error("No running builds")
1781 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001782
James E. Blairb536ecc2016-08-31 10:11:42 -07001783 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001784 """Assert that the completed builds are as described.
1785
1786 The list of completed builds is examined and must match
1787 exactly the list of builds described by the input.
1788
1789 :arg list history: A list of dictionaries. Each item in the
1790 list must match the corresponding build in the build
1791 history, and each element of the dictionary must match the
1792 corresponding attribute of the build.
1793
James E. Blairb536ecc2016-08-31 10:11:42 -07001794 :arg bool ordered: If true, the history must match the order
1795 supplied, if false, the builds are permitted to have
1796 arrived in any order.
1797
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001798 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001799 def matches(history_item, item):
1800 for k, v in item.items():
1801 if getattr(history_item, k) != v:
1802 return False
1803 return True
James E. Blair3158e282016-08-19 09:34:11 -07001804 try:
1805 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001806 if ordered:
1807 for i, d in enumerate(history):
1808 if not matches(self.history[i], d):
1809 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001810 "Element %i in history does not match %s" %
1811 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001812 else:
1813 unseen = self.history[:]
1814 for i, d in enumerate(history):
1815 found = False
1816 for unseen_item in unseen:
1817 if matches(unseen_item, d):
1818 found = True
1819 unseen.remove(unseen_item)
1820 break
1821 if not found:
1822 raise Exception("No match found for element %i "
1823 "in history" % (i,))
1824 if unseen:
1825 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001826 except Exception:
1827 for build in self.history:
1828 self.log.error("Completed build: %s" % build)
1829 else:
1830 self.log.error("No completed builds")
1831 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001832
James E. Blair6ac368c2016-12-22 18:07:20 -08001833 def printHistory(self):
1834 """Log the build history.
1835
1836 This can be useful during tests to summarize what jobs have
1837 completed.
1838
1839 """
1840 self.log.debug("Build history:")
1841 for build in self.history:
1842 self.log.debug(build)
1843
James E. Blair59fdbac2015-12-07 17:08:06 -08001844 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001845 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1846
1847 def updateConfigLayout(self, path):
1848 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001849 if not os.path.exists(root):
1850 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001851 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1852 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001853- tenant:
1854 name: openstack
1855 source:
1856 gerrit:
1857 config-repos:
1858 - %s
1859 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001860 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001861 self.config.set('zuul', 'tenant_config',
1862 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001863
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001864 def addCommitToRepo(self, project, message, files,
1865 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001866 path = os.path.join(self.upstream_root, project)
1867 repo = git.Repo(path)
1868 repo.head.reference = branch
1869 zuul.merger.merger.reset_repo_to_head(repo)
1870 for fn, content in files.items():
1871 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001872 try:
1873 os.makedirs(os.path.dirname(fn))
1874 except OSError:
1875 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001876 with open(fn, 'w') as f:
1877 f.write(content)
1878 repo.index.add([fn])
1879 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001880 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001881 repo.heads[branch].commit = commit
1882 repo.head.reference = branch
1883 repo.git.clean('-x', '-f', '-d')
1884 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001885 if tag:
1886 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001887 return before
1888
1889 def commitLayoutUpdate(self, orig_name, source_name):
1890 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07001891 source_name)
1892 to_copy = ['zuul.yaml']
1893 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
1894 to_copy.append('playbooks/{}'.format(playbook))
1895 commit_data = {}
1896 for source_file in to_copy:
1897 source_file_path = os.path.join(source_path, source_file)
1898 with open(source_file_path, 'r') as nt:
1899 commit_data[source_file] = nt.read()
1900 before = self.addCommitToRepo(
1901 orig_name, 'Pulling content from %s' % source_name,
1902 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08001903 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001904
James E. Blair7fc8daa2016-08-08 15:37:15 -07001905 def addEvent(self, connection, event):
1906 """Inject a Fake (Gerrit) event.
1907
1908 This method accepts a JSON-encoded event and simulates Zuul
1909 having received it from Gerrit. It could (and should)
1910 eventually apply to any connection type, but is currently only
1911 used with Gerrit connections. The name of the connection is
1912 used to look up the corresponding server, and the event is
1913 simulated as having been received by all Zuul connections
1914 attached to that server. So if two Gerrit connections in Zuul
1915 are connected to the same Gerrit server, and you invoke this
1916 method specifying the name of one of them, the event will be
1917 received by both.
1918
1919 .. note::
1920
1921 "self.fake_gerrit.addEvent" calls should be migrated to
1922 this method.
1923
1924 :arg str connection: The name of the connection corresponding
1925 to the gerrit server.
1926 :arg str event: The JSON-encoded event.
1927
1928 """
1929 specified_conn = self.connections.connections[connection]
1930 for conn in self.connections.connections.values():
1931 if (isinstance(conn, specified_conn.__class__) and
1932 specified_conn.server == conn.server):
1933 conn.addEvent(event)
1934
James E. Blair3f876d52016-07-22 13:07:14 -07001935
1936class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04001937 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001938 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11001939
Joshua Heskethd78b4482015-09-14 16:56:34 -06001940
1941class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08001942 def setup_config(self):
1943 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06001944 for section_name in self.config.sections():
1945 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1946 section_name, re.I)
1947 if not con_match:
1948 continue
1949
1950 if self.config.get(section_name, 'driver') == 'sql':
1951 f = MySQLSchemaFixture()
1952 self.useFixture(f)
1953 if (self.config.get(section_name, 'dburi') ==
1954 '$MYSQL_FIXTURE_DBURI$'):
1955 self.config.set(section_name, 'dburi', f.dburi)