blob: 9a6fb69155c3ebfced57c812f48749b76b510abb [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)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400804 for host in hosts:
805 host['host_vars']['ansible_connection'] = 'local'
806
807 hosts.append(dict(
808 name='localhost',
809 host_vars=dict(ansible_connection='local'),
810 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500811 return hosts
812
James E. Blairf5dbd002015-12-23 15:26:17 -0800813
Clark Boylanb640e052014-04-03 16:41:46 -0700814class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700815 """A Gearman server for use in tests.
816
817 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
818 added to the queue but will not be distributed to workers
819 until released. This attribute may be changed at any time and
820 will take effect for subsequently enqueued jobs, but
821 previously held jobs will still need to be explicitly
822 released.
823
824 """
825
Clark Boylanb640e052014-04-03 16:41:46 -0700826 def __init__(self):
827 self.hold_jobs_in_queue = False
828 super(FakeGearmanServer, self).__init__(0)
829
830 def getJobForConnection(self, connection, peek=False):
831 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
832 for job in queue:
833 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400834 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700835 job.waiting = self.hold_jobs_in_queue
836 else:
837 job.waiting = False
838 if job.waiting:
839 continue
840 if job.name in connection.functions:
841 if not peek:
842 queue.remove(job)
843 connection.related_jobs[job.handle] = job
844 job.worker_connection = connection
845 job.running = True
846 return job
847 return None
848
849 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700850 """Release a held job.
851
852 :arg str regex: A regular expression which, if supplied, will
853 cause only jobs with matching names to be released. If
854 not supplied, all jobs will be released.
855 """
Clark Boylanb640e052014-04-03 16:41:46 -0700856 released = False
857 qlen = (len(self.high_queue) + len(self.normal_queue) +
858 len(self.low_queue))
859 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
860 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400861 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700862 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500863 parameters = json.loads(job.arguments)
864 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700865 self.log.debug("releasing queued job %s" %
866 job.unique)
867 job.waiting = False
868 released = True
869 else:
870 self.log.debug("not releasing queued job %s" %
871 job.unique)
872 if released:
873 self.wakeConnections()
874 qlen = (len(self.high_queue) + len(self.normal_queue) +
875 len(self.low_queue))
876 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
877
878
879class FakeSMTP(object):
880 log = logging.getLogger('zuul.FakeSMTP')
881
882 def __init__(self, messages, server, port):
883 self.server = server
884 self.port = port
885 self.messages = messages
886
887 def sendmail(self, from_email, to_email, msg):
888 self.log.info("Sending email from %s, to %s, with msg %s" % (
889 from_email, to_email, msg))
890
891 headers = msg.split('\n\n', 1)[0]
892 body = msg.split('\n\n', 1)[1]
893
894 self.messages.append(dict(
895 from_email=from_email,
896 to_email=to_email,
897 msg=msg,
898 headers=headers,
899 body=body,
900 ))
901
902 return True
903
904 def quit(self):
905 return True
906
907
James E. Blairdce6cea2016-12-20 16:45:32 -0800908class FakeNodepool(object):
909 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800910 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800911
912 log = logging.getLogger("zuul.test.FakeNodepool")
913
914 def __init__(self, host, port, chroot):
915 self.client = kazoo.client.KazooClient(
916 hosts='%s:%s%s' % (host, port, chroot))
917 self.client.start()
918 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800919 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800920 self.thread = threading.Thread(target=self.run)
921 self.thread.daemon = True
922 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800923 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800924
925 def stop(self):
926 self._running = False
927 self.thread.join()
928 self.client.stop()
929 self.client.close()
930
931 def run(self):
932 while self._running:
933 self._run()
934 time.sleep(0.1)
935
936 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800937 if self.paused:
938 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800939 for req in self.getNodeRequests():
940 self.fulfillRequest(req)
941
942 def getNodeRequests(self):
943 try:
944 reqids = self.client.get_children(self.REQUEST_ROOT)
945 except kazoo.exceptions.NoNodeError:
946 return []
947 reqs = []
948 for oid in sorted(reqids):
949 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800950 try:
951 data, stat = self.client.get(path)
952 data = json.loads(data)
953 data['_oid'] = oid
954 reqs.append(data)
955 except kazoo.exceptions.NoNodeError:
956 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800957 return reqs
958
James E. Blaire18d4602017-01-05 11:17:28 -0800959 def getNodes(self):
960 try:
961 nodeids = self.client.get_children(self.NODE_ROOT)
962 except kazoo.exceptions.NoNodeError:
963 return []
964 nodes = []
965 for oid in sorted(nodeids):
966 path = self.NODE_ROOT + '/' + oid
967 data, stat = self.client.get(path)
968 data = json.loads(data)
969 data['_oid'] = oid
970 try:
971 lockfiles = self.client.get_children(path + '/lock')
972 except kazoo.exceptions.NoNodeError:
973 lockfiles = []
974 if lockfiles:
975 data['_lock'] = True
976 else:
977 data['_lock'] = False
978 nodes.append(data)
979 return nodes
980
James E. Blaira38c28e2017-01-04 10:33:20 -0800981 def makeNode(self, request_id, node_type):
982 now = time.time()
983 path = '/nodepool/nodes/'
984 data = dict(type=node_type,
985 provider='test-provider',
986 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -0400987 az='test-az',
James E. Blaira38c28e2017-01-04 10:33:20 -0800988 public_ipv4='127.0.0.1',
989 private_ipv4=None,
990 public_ipv6=None,
991 allocated_to=request_id,
992 state='ready',
993 state_time=now,
994 created_time=now,
995 updated_time=now,
996 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -0400997 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -0400998 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -0800999 data = json.dumps(data)
1000 path = self.client.create(path, data,
1001 makepath=True,
1002 sequence=True)
1003 nodeid = path.split("/")[-1]
1004 return nodeid
1005
James E. Blair6ab79e02017-01-06 10:10:17 -08001006 def addFailRequest(self, request):
1007 self.fail_requests.add(request['_oid'])
1008
James E. Blairdce6cea2016-12-20 16:45:32 -08001009 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001010 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001011 return
1012 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001013 oid = request['_oid']
1014 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001015
James E. Blair6ab79e02017-01-06 10:10:17 -08001016 if oid in self.fail_requests:
1017 request['state'] = 'failed'
1018 else:
1019 request['state'] = 'fulfilled'
1020 nodes = []
1021 for node in request['node_types']:
1022 nodeid = self.makeNode(oid, node)
1023 nodes.append(nodeid)
1024 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001025
James E. Blaira38c28e2017-01-04 10:33:20 -08001026 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001027 path = self.REQUEST_ROOT + '/' + oid
1028 data = json.dumps(request)
1029 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1030 self.client.set(path, data)
1031
1032
James E. Blair498059b2016-12-20 13:50:13 -08001033class ChrootedKazooFixture(fixtures.Fixture):
1034 def __init__(self):
1035 super(ChrootedKazooFixture, self).__init__()
1036
1037 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1038 if ':' in zk_host:
1039 host, port = zk_host.split(':')
1040 else:
1041 host = zk_host
1042 port = None
1043
1044 self.zookeeper_host = host
1045
1046 if not port:
1047 self.zookeeper_port = 2181
1048 else:
1049 self.zookeeper_port = int(port)
1050
1051 def _setUp(self):
1052 # Make sure the test chroot paths do not conflict
1053 random_bits = ''.join(random.choice(string.ascii_lowercase +
1054 string.ascii_uppercase)
1055 for x in range(8))
1056
1057 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1058 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1059
1060 # Ensure the chroot path exists and clean up any pre-existing znodes.
1061 _tmp_client = kazoo.client.KazooClient(
1062 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1063 _tmp_client.start()
1064
1065 if _tmp_client.exists(self.zookeeper_chroot):
1066 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1067
1068 _tmp_client.ensure_path(self.zookeeper_chroot)
1069 _tmp_client.stop()
1070 _tmp_client.close()
1071
1072 self.addCleanup(self._cleanup)
1073
1074 def _cleanup(self):
1075 '''Remove the chroot path.'''
1076 # Need a non-chroot'ed client to remove the chroot path
1077 _tmp_client = kazoo.client.KazooClient(
1078 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1079 _tmp_client.start()
1080 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1081 _tmp_client.stop()
1082
1083
Joshua Heskethd78b4482015-09-14 16:56:34 -06001084class MySQLSchemaFixture(fixtures.Fixture):
1085 def setUp(self):
1086 super(MySQLSchemaFixture, self).setUp()
1087
1088 random_bits = ''.join(random.choice(string.ascii_lowercase +
1089 string.ascii_uppercase)
1090 for x in range(8))
1091 self.name = '%s_%s' % (random_bits, os.getpid())
1092 self.passwd = uuid.uuid4().hex
1093 db = pymysql.connect(host="localhost",
1094 user="openstack_citest",
1095 passwd="openstack_citest",
1096 db="openstack_citest")
1097 cur = db.cursor()
1098 cur.execute("create database %s" % self.name)
1099 cur.execute(
1100 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1101 (self.name, self.name, self.passwd))
1102 cur.execute("flush privileges")
1103
1104 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1105 self.passwd,
1106 self.name)
1107 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1108 self.addCleanup(self.cleanup)
1109
1110 def cleanup(self):
1111 db = pymysql.connect(host="localhost",
1112 user="openstack_citest",
1113 passwd="openstack_citest",
1114 db="openstack_citest")
1115 cur = db.cursor()
1116 cur.execute("drop database %s" % self.name)
1117 cur.execute("drop user '%s'@'localhost'" % self.name)
1118 cur.execute("flush privileges")
1119
1120
Maru Newby3fe5f852015-01-13 04:22:14 +00001121class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001122 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001123 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001124
James E. Blair1c236df2017-02-01 14:07:24 -08001125 def attachLogs(self, *args):
1126 def reader():
1127 self._log_stream.seek(0)
1128 while True:
1129 x = self._log_stream.read(4096)
1130 if not x:
1131 break
1132 yield x.encode('utf8')
1133 content = testtools.content.content_from_reader(
1134 reader,
1135 testtools.content_type.UTF8_TEXT,
1136 False)
1137 self.addDetail('logging', content)
1138
Clark Boylanb640e052014-04-03 16:41:46 -07001139 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001140 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001141 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1142 try:
1143 test_timeout = int(test_timeout)
1144 except ValueError:
1145 # If timeout value is invalid do not set a timeout.
1146 test_timeout = 0
1147 if test_timeout > 0:
1148 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1149
1150 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1151 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1152 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1153 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1154 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1155 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1156 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1157 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1158 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1159 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001160 self._log_stream = StringIO()
1161 self.addOnException(self.attachLogs)
1162 else:
1163 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001164
James E. Blair1c236df2017-02-01 14:07:24 -08001165 handler = logging.StreamHandler(self._log_stream)
1166 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1167 '%(levelname)-8s %(message)s')
1168 handler.setFormatter(formatter)
1169
1170 logger = logging.getLogger()
1171 logger.setLevel(logging.DEBUG)
1172 logger.addHandler(handler)
1173
1174 # NOTE(notmorgan): Extract logging overrides for specific
1175 # libraries from the OS_LOG_DEFAULTS env and create loggers
1176 # for each. This is used to limit the output during test runs
1177 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001178 log_defaults_from_env = os.environ.get(
1179 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001180 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001181
James E. Blairdce6cea2016-12-20 16:45:32 -08001182 if log_defaults_from_env:
1183 for default in log_defaults_from_env.split(','):
1184 try:
1185 name, level_str = default.split('=', 1)
1186 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001187 logger = logging.getLogger(name)
1188 logger.setLevel(level)
1189 logger.addHandler(handler)
1190 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001191 except ValueError:
1192 # NOTE(notmorgan): Invalid format of the log default,
1193 # skip and don't try and apply a logger for the
1194 # specified module
1195 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001196
Maru Newby3fe5f852015-01-13 04:22:14 +00001197
1198class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001199 """A test case with a functioning Zuul.
1200
1201 The following class variables are used during test setup and can
1202 be overidden by subclasses but are effectively read-only once a
1203 test method starts running:
1204
1205 :cvar str config_file: This points to the main zuul config file
1206 within the fixtures directory. Subclasses may override this
1207 to obtain a different behavior.
1208
1209 :cvar str tenant_config_file: This is the tenant config file
1210 (which specifies from what git repos the configuration should
1211 be loaded). It defaults to the value specified in
1212 `config_file` but can be overidden by subclasses to obtain a
1213 different tenant/project layout while using the standard main
1214 configuration.
1215
1216 The following are instance variables that are useful within test
1217 methods:
1218
1219 :ivar FakeGerritConnection fake_<connection>:
1220 A :py:class:`~tests.base.FakeGerritConnection` will be
1221 instantiated for each connection present in the config file
1222 and stored here. For instance, `fake_gerrit` will hold the
1223 FakeGerritConnection object for a connection named `gerrit`.
1224
1225 :ivar FakeGearmanServer gearman_server: An instance of
1226 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1227 server that all of the Zuul components in this test use to
1228 communicate with each other.
1229
Paul Belanger174a8272017-03-14 13:20:10 -04001230 :ivar RecordingExecutorServer executor_server: An instance of
1231 :py:class:`~tests.base.RecordingExecutorServer` which is the
1232 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001233
1234 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1235 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001236 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001237 list upon completion.
1238
1239 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1240 objects representing completed builds. They are appended to
1241 the list in the order they complete.
1242
1243 """
1244
James E. Blair83005782015-12-11 14:46:03 -08001245 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001246 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001247
1248 def _startMerger(self):
1249 self.merge_server = zuul.merger.server.MergeServer(self.config,
1250 self.connections)
1251 self.merge_server.start()
1252
Maru Newby3fe5f852015-01-13 04:22:14 +00001253 def setUp(self):
1254 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001255
1256 self.setupZK()
1257
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001258 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001259 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001260 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1261 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001262 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001263 tmp_root = tempfile.mkdtemp(
1264 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001265 self.test_root = os.path.join(tmp_root, "zuul-test")
1266 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001267 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001268 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001269 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001270
1271 if os.path.exists(self.test_root):
1272 shutil.rmtree(self.test_root)
1273 os.makedirs(self.test_root)
1274 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001275 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001276
1277 # Make per test copy of Configuration.
1278 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001279 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001280 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001281 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001282 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001283 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001284 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001285
1286 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001287 # TODOv3(jeblair): remove these and replace with new git
1288 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001289 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001290 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001291 self.init_repo("org/project5")
1292 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001293 self.init_repo("org/one-job-project")
1294 self.init_repo("org/nonvoting-project")
1295 self.init_repo("org/templated-project")
1296 self.init_repo("org/layered-project")
1297 self.init_repo("org/node-project")
1298 self.init_repo("org/conflict-project")
1299 self.init_repo("org/noop-project")
1300 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001301 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001302
1303 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001304 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1305 # see: https://github.com/jsocol/pystatsd/issues/61
1306 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001307 os.environ['STATSD_PORT'] = str(self.statsd.port)
1308 self.statsd.start()
1309 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001310 reload_module(statsd)
1311 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001312
1313 self.gearman_server = FakeGearmanServer()
1314
1315 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001316 self.log.info("Gearman server on port %s" %
1317 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001318
James E. Blaire511d2f2016-12-08 15:22:26 -08001319 gerritsource.GerritSource.replication_timeout = 1.5
1320 gerritsource.GerritSource.replication_retry_interval = 0.5
1321 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001322
Joshua Hesketh352264b2015-08-11 23:42:08 +10001323 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001324
Jan Hruban6b71aff2015-10-22 16:58:08 +02001325 self.event_queues = [
1326 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001327 self.sched.trigger_event_queue,
1328 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001329 ]
1330
James E. Blairfef78942016-03-11 16:28:56 -08001331 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001332 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001333
Clark Boylanb640e052014-04-03 16:41:46 -07001334 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001335 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001336 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001337 return FakeURLOpener(self.upstream_root, *args, **kw)
1338
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001339 old_urlopen = urllib.request.urlopen
1340 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001341
James E. Blair3f876d52016-07-22 13:07:14 -07001342 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001343
Paul Belanger174a8272017-03-14 13:20:10 -04001344 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001345 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001346 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001347 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001348 _test_root=self.test_root,
1349 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001350 self.executor_server.start()
1351 self.history = self.executor_server.build_history
1352 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001353
Paul Belanger174a8272017-03-14 13:20:10 -04001354 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001355 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001356 self.merge_client = zuul.merger.client.MergeClient(
1357 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001358 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001359 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001360 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001361
James E. Blair0d5a36e2017-02-21 10:53:44 -05001362 self.fake_nodepool = FakeNodepool(
1363 self.zk_chroot_fixture.zookeeper_host,
1364 self.zk_chroot_fixture.zookeeper_port,
1365 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001366
Paul Belanger174a8272017-03-14 13:20:10 -04001367 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001368 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001369 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001370 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001371
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001372 self.webapp = zuul.webapp.WebApp(
1373 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001374 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001375
1376 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001377 self.webapp.start()
1378 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001379 self.executor_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001380 self.addCleanup(self.shutdown)
1381
James E. Blairb9c0d772017-03-03 14:34:49 -08001382 self.sched.reconfigure(self.config)
1383 self.sched.resume()
1384
James E. Blaire18d4602017-01-05 11:17:28 -08001385 def tearDown(self):
1386 super(ZuulTestCase, self).tearDown()
1387 self.assertFinalState()
1388
James E. Blairfef78942016-03-11 16:28:56 -08001389 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001390 # Set up gerrit related fakes
1391 # Set a changes database so multiple FakeGerrit's can report back to
1392 # a virtual canonical database given by the configured hostname
1393 self.gerrit_changes_dbs = {}
1394
1395 def getGerritConnection(driver, name, config):
1396 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1397 con = FakeGerritConnection(driver, name, config,
1398 changes_db=db,
1399 upstream_root=self.upstream_root)
1400 self.event_queues.append(con.event_queue)
1401 setattr(self, 'fake_' + name, con)
1402 return con
1403
1404 self.useFixture(fixtures.MonkeyPatch(
1405 'zuul.driver.gerrit.GerritDriver.getConnection',
1406 getGerritConnection))
1407
1408 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001409 # TODO(jhesketh): This should come from lib.connections for better
1410 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001411 # Register connections from the config
1412 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001413
Joshua Hesketh352264b2015-08-11 23:42:08 +10001414 def FakeSMTPFactory(*args, **kw):
1415 args = [self.smtp_messages] + list(args)
1416 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001417
Joshua Hesketh352264b2015-08-11 23:42:08 +10001418 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001419
James E. Blaire511d2f2016-12-08 15:22:26 -08001420 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001421 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001422 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001423
James E. Blair83005782015-12-11 14:46:03 -08001424 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001425 # This creates the per-test configuration object. It can be
1426 # overriden by subclasses, but should not need to be since it
1427 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001428 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001429 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001430 if hasattr(self, 'tenant_config_file'):
1431 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001432 git_path = os.path.join(
1433 os.path.dirname(
1434 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1435 'git')
1436 if os.path.exists(git_path):
1437 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001438 project = reponame.replace('_', '/')
1439 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001440 os.path.join(git_path, reponame))
1441
James E. Blair498059b2016-12-20 13:50:13 -08001442 def setupZK(self):
1443 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001444 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001445 self.zk_chroot_fixture.zookeeper_host,
1446 self.zk_chroot_fixture.zookeeper_port,
1447 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001448
James E. Blair96c6bf82016-01-15 16:20:40 -08001449 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001450 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001451
1452 files = {}
1453 for (dirpath, dirnames, filenames) in os.walk(source_path):
1454 for filename in filenames:
1455 test_tree_filepath = os.path.join(dirpath, filename)
1456 common_path = os.path.commonprefix([test_tree_filepath,
1457 source_path])
1458 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1459 with open(test_tree_filepath, 'r') as f:
1460 content = f.read()
1461 files[relative_filepath] = content
1462 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001463 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001464
James E. Blaire18d4602017-01-05 11:17:28 -08001465 def assertNodepoolState(self):
1466 # Make sure that there are no pending requests
1467
1468 requests = self.fake_nodepool.getNodeRequests()
1469 self.assertEqual(len(requests), 0)
1470
1471 nodes = self.fake_nodepool.getNodes()
1472 for node in nodes:
1473 self.assertFalse(node['_lock'], "Node %s is locked" %
1474 (node['_oid'],))
1475
Clark Boylanb640e052014-04-03 16:41:46 -07001476 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001477 # Make sure that git.Repo objects have been garbage collected.
1478 repos = []
1479 gc.collect()
1480 for obj in gc.get_objects():
1481 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001482 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001483 repos.append(obj)
1484 self.assertEqual(len(repos), 0)
1485 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001486 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001487 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001488 for tenant in self.sched.abide.tenants.values():
1489 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001490 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001491 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001492
1493 def shutdown(self):
1494 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001495 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001496 self.merge_server.stop()
1497 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001498 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001499 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001500 self.sched.stop()
1501 self.sched.join()
1502 self.statsd.stop()
1503 self.statsd.join()
1504 self.webapp.stop()
1505 self.webapp.join()
1506 self.rpc.stop()
1507 self.rpc.join()
1508 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001509 self.fake_nodepool.stop()
1510 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001511 threads = threading.enumerate()
1512 if len(threads) > 1:
1513 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001514 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001515
1516 def init_repo(self, project):
1517 parts = project.split('/')
1518 path = os.path.join(self.upstream_root, *parts[:-1])
1519 if not os.path.exists(path):
1520 os.makedirs(path)
1521 path = os.path.join(self.upstream_root, project)
1522 repo = git.Repo.init(path)
1523
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001524 with repo.config_writer() as config_writer:
1525 config_writer.set_value('user', 'email', 'user@example.com')
1526 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001527
Clark Boylanb640e052014-04-03 16:41:46 -07001528 repo.index.commit('initial commit')
1529 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001530
James E. Blair97d902e2014-08-21 13:25:56 -07001531 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001532 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001533 repo.git.clean('-x', '-f', '-d')
1534
James E. Blair97d902e2014-08-21 13:25:56 -07001535 def create_branch(self, project, branch):
1536 path = os.path.join(self.upstream_root, project)
1537 repo = git.Repo.init(path)
1538 fn = os.path.join(path, 'README')
1539
1540 branch_head = repo.create_head(branch)
1541 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001542 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001543 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001544 f.close()
1545 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001546 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001547
James E. Blair97d902e2014-08-21 13:25:56 -07001548 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001549 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001550 repo.git.clean('-x', '-f', '-d')
1551
Sachi King9f16d522016-03-16 12:20:45 +11001552 def create_commit(self, project):
1553 path = os.path.join(self.upstream_root, project)
1554 repo = git.Repo(path)
1555 repo.head.reference = repo.heads['master']
1556 file_name = os.path.join(path, 'README')
1557 with open(file_name, 'a') as f:
1558 f.write('creating fake commit\n')
1559 repo.index.add([file_name])
1560 commit = repo.index.commit('Creating a fake commit')
1561 return commit.hexsha
1562
James E. Blairb8c16472015-05-05 14:55:26 -07001563 def orderedRelease(self):
1564 # Run one build at a time to ensure non-race order:
1565 while len(self.builds):
1566 self.release(self.builds[0])
1567 self.waitUntilSettled()
1568
Clark Boylanb640e052014-04-03 16:41:46 -07001569 def release(self, job):
1570 if isinstance(job, FakeBuild):
1571 job.release()
1572 else:
1573 job.waiting = False
1574 self.log.debug("Queued job %s released" % job.unique)
1575 self.gearman_server.wakeConnections()
1576
1577 def getParameter(self, job, name):
1578 if isinstance(job, FakeBuild):
1579 return job.parameters[name]
1580 else:
1581 parameters = json.loads(job.arguments)
1582 return parameters[name]
1583
Clark Boylanb640e052014-04-03 16:41:46 -07001584 def haveAllBuildsReported(self):
1585 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001586 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001587 return False
1588 # Find out if every build that the worker has completed has been
1589 # reported back to Zuul. If it hasn't then that means a Gearman
1590 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001591 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001592 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001593 if not zbuild:
1594 # It has already been reported
1595 continue
1596 # It hasn't been reported yet.
1597 return False
1598 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001599 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001600 if connection.state == 'GRAB_WAIT':
1601 return False
1602 return True
1603
1604 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001605 builds = self.executor_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001606 for build in builds:
1607 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001608 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001609 for j in conn.related_jobs.values():
1610 if j.unique == build.uuid:
1611 client_job = j
1612 break
1613 if not client_job:
1614 self.log.debug("%s is not known to the gearman client" %
1615 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001616 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001617 if not client_job.handle:
1618 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001619 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001620 server_job = self.gearman_server.jobs.get(client_job.handle)
1621 if not server_job:
1622 self.log.debug("%s is not known to the gearman server" %
1623 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001624 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001625 if not hasattr(server_job, 'waiting'):
1626 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001627 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001628 if server_job.waiting:
1629 continue
James E. Blair17302972016-08-10 16:11:42 -07001630 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001631 self.log.debug("%s has not reported start" % build)
1632 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001633 worker_build = self.executor_server.job_builds.get(
1634 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001635 if worker_build:
1636 if worker_build.isWaiting():
1637 continue
1638 else:
1639 self.log.debug("%s is running" % worker_build)
1640 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001641 else:
James E. Blair962220f2016-08-03 11:22:38 -07001642 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001643 return False
1644 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001645
James E. Blairdce6cea2016-12-20 16:45:32 -08001646 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001647 if self.fake_nodepool.paused:
1648 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001649 if self.sched.nodepool.requests:
1650 return False
1651 return True
1652
Jan Hruban6b71aff2015-10-22 16:58:08 +02001653 def eventQueuesEmpty(self):
1654 for queue in self.event_queues:
1655 yield queue.empty()
1656
1657 def eventQueuesJoin(self):
1658 for queue in self.event_queues:
1659 queue.join()
1660
Clark Boylanb640e052014-04-03 16:41:46 -07001661 def waitUntilSettled(self):
1662 self.log.debug("Waiting until settled...")
1663 start = time.time()
1664 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001665 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001666 self.log.error("Timeout waiting for Zuul to settle")
1667 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001668 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001669 self.log.error(" %s: %s" % (queue, queue.empty()))
1670 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001671 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001672 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001673 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001674 self.log.error("All requests completed: %s" %
1675 (self.areAllNodeRequestsComplete(),))
1676 self.log.error("Merge client jobs: %s" %
1677 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001678 raise Exception("Timeout waiting for Zuul to settle")
1679 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001680
Paul Belanger174a8272017-03-14 13:20:10 -04001681 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001682 # have all build states propogated to zuul?
1683 if self.haveAllBuildsReported():
1684 # Join ensures that the queue is empty _and_ events have been
1685 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001686 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001687 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001688 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001689 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001690 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001691 self.areAllNodeRequestsComplete() and
1692 all(self.eventQueuesEmpty())):
1693 # The queue empty check is placed at the end to
1694 # ensure that if a component adds an event between
1695 # when locked the run handler and checked that the
1696 # components were stable, we don't erroneously
1697 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001698 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001699 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001700 self.log.debug("...settled.")
1701 return
1702 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001703 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001704 self.sched.wake_event.wait(0.1)
1705
1706 def countJobResults(self, jobs, result):
1707 jobs = filter(lambda x: x.result == result, jobs)
1708 return len(jobs)
1709
James E. Blair96c6bf82016-01-15 16:20:40 -08001710 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001711 for job in self.history:
1712 if (job.name == name and
1713 (project is None or
1714 job.parameters['ZUUL_PROJECT'] == project)):
1715 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001716 raise Exception("Unable to find job %s in history" % name)
1717
1718 def assertEmptyQueues(self):
1719 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001720 for tenant in self.sched.abide.tenants.values():
1721 for pipeline in tenant.layout.pipelines.values():
1722 for queue in pipeline.queues:
1723 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001724 print('pipeline %s queue %s contents %s' % (
1725 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001726 self.assertEqual(len(queue.queue), 0,
1727 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001728
1729 def assertReportedStat(self, key, value=None, kind=None):
1730 start = time.time()
1731 while time.time() < (start + 5):
1732 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001733 k, v = stat.split(':')
1734 if key == k:
1735 if value is None and kind is None:
1736 return
1737 elif value:
1738 if value == v:
1739 return
1740 elif kind:
1741 if v.endswith('|' + kind):
1742 return
1743 time.sleep(0.1)
1744
Clark Boylanb640e052014-04-03 16:41:46 -07001745 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001746
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001747 def assertBuilds(self, builds):
1748 """Assert that the running builds are as described.
1749
1750 The list of running builds is examined and must match exactly
1751 the list of builds described by the input.
1752
1753 :arg list builds: A list of dictionaries. Each item in the
1754 list must match the corresponding build in the build
1755 history, and each element of the dictionary must match the
1756 corresponding attribute of the build.
1757
1758 """
James E. Blair3158e282016-08-19 09:34:11 -07001759 try:
1760 self.assertEqual(len(self.builds), len(builds))
1761 for i, d in enumerate(builds):
1762 for k, v in d.items():
1763 self.assertEqual(
1764 getattr(self.builds[i], k), v,
1765 "Element %i in builds does not match" % (i,))
1766 except Exception:
1767 for build in self.builds:
1768 self.log.error("Running build: %s" % build)
1769 else:
1770 self.log.error("No running builds")
1771 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001772
James E. Blairb536ecc2016-08-31 10:11:42 -07001773 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001774 """Assert that the completed builds are as described.
1775
1776 The list of completed builds is examined and must match
1777 exactly the list of builds described by the input.
1778
1779 :arg list history: A list of dictionaries. Each item in the
1780 list must match the corresponding build in the build
1781 history, and each element of the dictionary must match the
1782 corresponding attribute of the build.
1783
James E. Blairb536ecc2016-08-31 10:11:42 -07001784 :arg bool ordered: If true, the history must match the order
1785 supplied, if false, the builds are permitted to have
1786 arrived in any order.
1787
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001788 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001789 def matches(history_item, item):
1790 for k, v in item.items():
1791 if getattr(history_item, k) != v:
1792 return False
1793 return True
James E. Blair3158e282016-08-19 09:34:11 -07001794 try:
1795 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001796 if ordered:
1797 for i, d in enumerate(history):
1798 if not matches(self.history[i], d):
1799 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001800 "Element %i in history does not match %s" %
1801 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001802 else:
1803 unseen = self.history[:]
1804 for i, d in enumerate(history):
1805 found = False
1806 for unseen_item in unseen:
1807 if matches(unseen_item, d):
1808 found = True
1809 unseen.remove(unseen_item)
1810 break
1811 if not found:
1812 raise Exception("No match found for element %i "
1813 "in history" % (i,))
1814 if unseen:
1815 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001816 except Exception:
1817 for build in self.history:
1818 self.log.error("Completed build: %s" % build)
1819 else:
1820 self.log.error("No completed builds")
1821 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001822
James E. Blair6ac368c2016-12-22 18:07:20 -08001823 def printHistory(self):
1824 """Log the build history.
1825
1826 This can be useful during tests to summarize what jobs have
1827 completed.
1828
1829 """
1830 self.log.debug("Build history:")
1831 for build in self.history:
1832 self.log.debug(build)
1833
James E. Blair59fdbac2015-12-07 17:08:06 -08001834 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001835 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1836
1837 def updateConfigLayout(self, path):
1838 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001839 if not os.path.exists(root):
1840 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001841 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1842 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001843- tenant:
1844 name: openstack
1845 source:
1846 gerrit:
1847 config-repos:
1848 - %s
1849 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001850 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001851 self.config.set('zuul', 'tenant_config',
1852 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001853
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001854 def addCommitToRepo(self, project, message, files,
1855 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001856 path = os.path.join(self.upstream_root, project)
1857 repo = git.Repo(path)
1858 repo.head.reference = branch
1859 zuul.merger.merger.reset_repo_to_head(repo)
1860 for fn, content in files.items():
1861 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001862 try:
1863 os.makedirs(os.path.dirname(fn))
1864 except OSError:
1865 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001866 with open(fn, 'w') as f:
1867 f.write(content)
1868 repo.index.add([fn])
1869 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001870 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001871 repo.heads[branch].commit = commit
1872 repo.head.reference = branch
1873 repo.git.clean('-x', '-f', '-d')
1874 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001875 if tag:
1876 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001877 return before
1878
1879 def commitLayoutUpdate(self, orig_name, source_name):
1880 source_path = os.path.join(self.test_root, 'upstream',
1881 source_name, 'zuul.yaml')
1882 with open(source_path, 'r') as nt:
1883 before = self.addCommitToRepo(
1884 orig_name, 'Pulling content from %s' % source_name,
1885 {'zuul.yaml': nt.read()})
1886 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001887
James E. Blair7fc8daa2016-08-08 15:37:15 -07001888 def addEvent(self, connection, event):
1889 """Inject a Fake (Gerrit) event.
1890
1891 This method accepts a JSON-encoded event and simulates Zuul
1892 having received it from Gerrit. It could (and should)
1893 eventually apply to any connection type, but is currently only
1894 used with Gerrit connections. The name of the connection is
1895 used to look up the corresponding server, and the event is
1896 simulated as having been received by all Zuul connections
1897 attached to that server. So if two Gerrit connections in Zuul
1898 are connected to the same Gerrit server, and you invoke this
1899 method specifying the name of one of them, the event will be
1900 received by both.
1901
1902 .. note::
1903
1904 "self.fake_gerrit.addEvent" calls should be migrated to
1905 this method.
1906
1907 :arg str connection: The name of the connection corresponding
1908 to the gerrit server.
1909 :arg str event: The JSON-encoded event.
1910
1911 """
1912 specified_conn = self.connections.connections[connection]
1913 for conn in self.connections.connections.values():
1914 if (isinstance(conn, specified_conn.__class__) and
1915 specified_conn.server == conn.server):
1916 conn.addEvent(event)
1917
James E. Blair3f876d52016-07-22 13:07:14 -07001918
1919class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04001920 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001921 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11001922
Joshua Heskethd78b4482015-09-14 16:56:34 -06001923
1924class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08001925 def setup_config(self):
1926 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06001927 for section_name in self.config.sections():
1928 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1929 section_name, re.I)
1930 if not con_match:
1931 continue
1932
1933 if self.config.get(section_name, 'driver') == 'sql':
1934 f = MySQLSchemaFixture()
1935 self.useFixture(f)
1936 if (self.config.get(section_name, 'dburi') ==
1937 '$MYSQL_FIXTURE_DBURI$'):
1938 self.config.set(section_name, 'dburi', f.dburi)