blob: 16619571687dceceae7a74473b3f09f375ac36a5 [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.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
Christian Berendtffba5df2014-06-07 21:30:22 +020017from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070018import gc
19import hashlib
20import json
21import logging
22import os
23import pprint
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
Clark Boylanb640e052014-04-03 16:41:46 -070031import socket
32import string
33import subprocess
34import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080035import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070036import threading
37import time
Clark Boylanb640e052014-04-03 16:41:46 -070038
39import git
40import gear
41import fixtures
Clark Boylanb640e052014-04-03 16:41:46 -070042import statsd
43import testtools
Mike Heald8225f522014-11-21 09:52:33 +000044from git import GitCommandError
Clark Boylanb640e052014-04-03 16:41:46 -070045
Joshua Hesketh352264b2015-08-11 23:42:08 +100046import zuul.connection.gerrit
47import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070048import zuul.scheduler
49import zuul.webapp
50import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100051import zuul.launcher.server
52import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070053import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080054import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070056import zuul.merger.merger
57import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070058import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.reporter.gerrit
60import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110061import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.trigger.gerrit
63import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070064import zuul.trigger.zuultrigger
Clark Boylanb640e052014-04-03 16:41:46 -070065
66FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
67 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070068USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070069
70logging.basicConfig(level=logging.DEBUG,
71 format='%(asctime)s %(name)-32s '
72 '%(levelname)-8s %(message)s')
73
74
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700106 categories = {'approved': ('Approved', -1, 1),
107 'code-review': ('Code-Review', -2, 2),
108 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700263 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
Joshua Hesketh642824b2014-07-01 17:54:59 +1000269 def addApproval(self, category, value, username='reviewer_john',
270 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700271 if not granted_on:
272 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000273 approval = {
274 'description': self.categories[category][0],
275 'type': category,
276 'value': str(value),
277 'by': {
278 'username': username,
279 'email': username + '@example.com',
280 },
281 'grantedOn': int(granted_on)
282 }
Clark Boylanb640e052014-04-03 16:41:46 -0700283 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
284 if x['by']['username'] == username and x['type'] == category:
285 del self.patchsets[-1]['approvals'][i]
286 self.patchsets[-1]['approvals'].append(approval)
287 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000288 'author': {'email': 'author@example.com',
289 'name': 'Patchset Author',
290 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700291 'change': {'branch': self.branch,
292 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
293 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000294 'owner': {'email': 'owner@example.com',
295 'name': 'Change Owner',
296 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700297 'project': self.project,
298 'subject': self.subject,
299 'topic': 'master',
300 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000301 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700302 'patchSet': self.patchsets[-1],
303 'type': 'comment-added'}
304 self.data['submitRecords'] = self.getSubmitRecords()
305 return json.loads(json.dumps(event))
306
307 def getSubmitRecords(self):
308 status = {}
309 for cat in self.categories.keys():
310 status[cat] = 0
311
312 for a in self.patchsets[-1]['approvals']:
313 cur = status[a['type']]
314 cat_min, cat_max = self.categories[a['type']][1:]
315 new = int(a['value'])
316 if new == cat_min:
317 cur = new
318 elif abs(new) > abs(cur):
319 cur = new
320 status[a['type']] = cur
321
322 labels = []
323 ok = True
324 for typ, cat in self.categories.items():
325 cur = status[typ]
326 cat_min, cat_max = cat[1:]
327 if cur == cat_min:
328 value = 'REJECT'
329 ok = False
330 elif cur == cat_max:
331 value = 'OK'
332 else:
333 value = 'NEED'
334 ok = False
335 labels.append({'label': cat[0], 'status': value})
336 if ok:
337 return [{'status': 'OK'}]
338 return [{'status': 'NOT_READY',
339 'labels': labels}]
340
341 def setDependsOn(self, other, patchset):
342 self.depends_on_change = other
343 d = {'id': other.data['id'],
344 'number': other.data['number'],
345 'ref': other.patchsets[patchset - 1]['ref']
346 }
347 self.data['dependsOn'] = [d]
348
349 other.needed_by_changes.append(self)
350 needed = other.data.get('neededBy', [])
351 d = {'id': self.data['id'],
352 'number': self.data['number'],
353 'ref': self.patchsets[patchset - 1]['ref'],
354 'revision': self.patchsets[patchset - 1]['revision']
355 }
356 needed.append(d)
357 other.data['neededBy'] = needed
358
359 def query(self):
360 self.queried += 1
361 d = self.data.get('dependsOn')
362 if d:
363 d = d[0]
364 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
365 d['isCurrentPatchSet'] = True
366 else:
367 d['isCurrentPatchSet'] = False
368 return json.loads(json.dumps(self.data))
369
370 def setMerged(self):
371 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000372 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700373 return
374 if self.fail_merge:
375 return
376 self.data['status'] = 'MERGED'
377 self.open = False
378
379 path = os.path.join(self.upstream_root, self.project)
380 repo = git.Repo(path)
381 repo.heads[self.branch].commit = \
382 repo.commit(self.patchsets[-1]['revision'])
383
384 def setReported(self):
385 self.reported += 1
386
387
Joshua Hesketh352264b2015-08-11 23:42:08 +1000388class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700389 """A Fake Gerrit connection for use in tests.
390
391 This subclasses
392 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
393 ability for tests to add changes to the fake Gerrit it represents.
394 """
395
Joshua Hesketh352264b2015-08-11 23:42:08 +1000396 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700397
Joshua Hesketh352264b2015-08-11 23:42:08 +1000398 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700399 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000400 super(FakeGerritConnection, self).__init__(connection_name,
401 connection_config)
402
James E. Blair7fc8daa2016-08-08 15:37:15 -0700403 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700404 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
405 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000406 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700407 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200408 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700409
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700410 def addFakeChange(self, project, branch, subject, status='NEW',
411 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700412 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700413 self.change_number += 1
414 c = FakeChange(self, self.change_number, project, branch, subject,
415 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700416 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700417 self.changes[self.change_number] = c
418 return c
419
Clark Boylanb640e052014-04-03 16:41:46 -0700420 def review(self, project, changeid, message, action):
421 number, ps = changeid.split(',')
422 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000423
424 # Add the approval back onto the change (ie simulate what gerrit would
425 # do).
426 # Usually when zuul leaves a review it'll create a feedback loop where
427 # zuul's review enters another gerrit event (which is then picked up by
428 # zuul). However, we can't mimic this behaviour (by adding this
429 # approval event into the queue) as it stops jobs from checking what
430 # happens before this event is triggered. If a job needs to see what
431 # happens they can add their own verified event into the queue.
432 # Nevertheless, we can update change with the new review in gerrit.
433
James E. Blair8b5408c2016-08-08 15:37:46 -0700434 for cat in action.keys():
435 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000436 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000437
James E. Blair8b5408c2016-08-08 15:37:46 -0700438 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000439 if 'label' in action:
440 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000441 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000442
Clark Boylanb640e052014-04-03 16:41:46 -0700443 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000444
Clark Boylanb640e052014-04-03 16:41:46 -0700445 if 'submit' in action:
446 change.setMerged()
447 if message:
448 change.setReported()
449
450 def query(self, number):
451 change = self.changes.get(int(number))
452 if change:
453 return change.query()
454 return {}
455
James E. Blairc494d542014-08-06 09:23:52 -0700456 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700457 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700458 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800459 if query.startswith('change:'):
460 # Query a specific changeid
461 changeid = query[len('change:'):]
462 l = [change.query() for change in self.changes.values()
463 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700464 elif query.startswith('message:'):
465 # Query the content of a commit message
466 msg = query[len('message:'):].strip()
467 l = [change.query() for change in self.changes.values()
468 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800469 else:
470 # Query all open changes
471 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700472 return l
James E. Blairc494d542014-08-06 09:23:52 -0700473
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700475 pass
476
Joshua Hesketh352264b2015-08-11 23:42:08 +1000477 def getGitUrl(self, project):
478 return os.path.join(self.upstream_root, project.name)
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480
481class BuildHistory(object):
482 def __init__(self, **kw):
483 self.__dict__.update(kw)
484
485 def __repr__(self):
486 return ("<Completed build, result: %s name: %s #%s changes: %s>" %
487 (self.result, self.name, self.number, self.changes))
488
489
490class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200491 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700492 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700493 self.url = url
494
495 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700496 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700497 path = res.path
498 project = '/'.join(path.split('/')[2:-2])
499 ret = '001e# service=git-upload-pack\n'
500 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
501 'multi_ack thin-pack side-band side-band-64k ofs-delta '
502 'shallow no-progress include-tag multi_ack_detailed no-done\n')
503 path = os.path.join(self.upstream_root, project)
504 repo = git.Repo(path)
505 for ref in repo.refs:
506 r = ref.object.hexsha + ' ' + ref.path + '\n'
507 ret += '%04x%s' % (len(r) + 4, r)
508 ret += '0000'
509 return ret
510
511
Clark Boylanb640e052014-04-03 16:41:46 -0700512class FakeStatsd(threading.Thread):
513 def __init__(self):
514 threading.Thread.__init__(self)
515 self.daemon = True
516 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
517 self.sock.bind(('', 0))
518 self.port = self.sock.getsockname()[1]
519 self.wake_read, self.wake_write = os.pipe()
520 self.stats = []
521
522 def run(self):
523 while True:
524 poll = select.poll()
525 poll.register(self.sock, select.POLLIN)
526 poll.register(self.wake_read, select.POLLIN)
527 ret = poll.poll()
528 for (fd, event) in ret:
529 if fd == self.sock.fileno():
530 data = self.sock.recvfrom(1024)
531 if not data:
532 return
533 self.stats.append(data[0])
534 if fd == self.wake_read:
535 return
536
537 def stop(self):
538 os.write(self.wake_write, '1\n')
539
540
James E. Blaire1767bc2016-08-02 10:00:27 -0700541class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700542 log = logging.getLogger("zuul.test")
543
James E. Blairab7132b2016-08-05 12:36:22 -0700544 def __init__(self, launch_server, job, number, node):
Clark Boylanb640e052014-04-03 16:41:46 -0700545 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700546 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700547 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700548 self.jobdir = None
Clark Boylanb640e052014-04-03 16:41:46 -0700549 self.number = number
550 self.node = node
551 self.parameters = json.loads(job.arguments)
552 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700553 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700554 self.wait_condition = threading.Condition()
555 self.waiting = False
556 self.aborted = False
557 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700558 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700559 self.changes = None
560 if 'ZUUL_CHANGE_IDS' in self.parameters:
561 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700562
563 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700564 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700565 self.wait_condition.acquire()
566 self.wait_condition.notify()
567 self.waiting = False
568 self.log.debug("Build %s released" % self.unique)
569 self.wait_condition.release()
570
571 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700572 """Return whether this build is being held.
573
574 :returns: Whether the build is being held.
575 :rtype: bool
576 """
577
Clark Boylanb640e052014-04-03 16:41:46 -0700578 self.wait_condition.acquire()
579 if self.waiting:
580 ret = True
581 else:
582 ret = False
583 self.wait_condition.release()
584 return ret
585
586 def _wait(self):
587 self.wait_condition.acquire()
588 self.waiting = True
589 self.log.debug("Build %s waiting" % self.unique)
590 self.wait_condition.wait()
591 self.wait_condition.release()
592
593 def run(self):
594 data = {
595 'url': 'https://server/job/%s/%s/' % (self.name, self.number),
596 'name': self.name,
597 'number': self.number,
James E. Blaire1767bc2016-08-02 10:00:27 -0700598 'manager': self.launch_server.worker.worker_id,
Clark Boylanb640e052014-04-03 16:41:46 -0700599 'worker_name': 'My Worker',
600 'worker_hostname': 'localhost',
601 'worker_ips': ['127.0.0.1', '192.168.1.1'],
602 'worker_fqdn': 'zuul.example.org',
603 'worker_program': 'FakeBuilder',
604 'worker_version': 'v1.1',
605 'worker_extra': {'something': 'else'}
606 }
607
608 self.log.debug('Running build %s' % self.unique)
609
610 self.job.sendWorkData(json.dumps(data))
611 self.log.debug('Sent WorkData packet with %s' % json.dumps(data))
612 self.job.sendWorkStatus(0, 100)
613
James E. Blaire1767bc2016-08-02 10:00:27 -0700614 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700615 self.log.debug('Holding build %s' % self.unique)
616 self._wait()
617 self.log.debug("Build %s continuing" % self.unique)
618
Clark Boylanb640e052014-04-03 16:41:46 -0700619 result = 'SUCCESS'
620 if (('ZUUL_REF' in self.parameters) and
James E. Blaire1767bc2016-08-02 10:00:27 -0700621 self.launch_server.shouldFailTest(self.name,
622 self.parameters['ZUUL_REF'])):
Clark Boylanb640e052014-04-03 16:41:46 -0700623 result = 'FAILURE'
624 if self.aborted:
625 result = 'ABORTED'
626
627 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700628 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700629
James E. Blaire1767bc2016-08-02 10:00:27 -0700630 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700631
James E. Blaire7b99a02016-08-05 14:27:34 -0700632 def hasChanges(self, *changes):
633 """Return whether this build has certain changes in its git repos.
634
635 :arg FakeChange changes: One or more changes (varargs) that
636 are expected to be present (in order) in the git repository of
637 the active project.
638
639 :returns: Whether the build has the indicated changes.
640 :rtype: bool
641
642 """
James E. Blair962220f2016-08-03 11:22:38 -0700643 project = self.parameters['ZUUL_PROJECT']
644 path = os.path.join(self.jobdir.git_root, project)
645 repo = git.Repo(path)
646 ref = self.parameters['ZUUL_REF']
647 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
James E. Blaire7b99a02016-08-05 14:27:34 -0700648 commit_messages = ['%s-1' % change.subject for change in changes]
James E. Blair962220f2016-08-03 11:22:38 -0700649 self.log.debug("Checking if build %s has changes; commit_messages %s;"
650 " repo_messages %s" % (self, commit_messages,
651 repo_messages))
652 for msg in commit_messages:
653 if msg not in repo_messages:
654 self.log.debug(" messages do not match")
655 return False
656 self.log.debug(" OK")
657 return True
658
Clark Boylanb640e052014-04-03 16:41:46 -0700659
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000660class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700661 """An Ansible launcher to be used in tests.
662
663 :ivar bool hold_jobs_in_build: If true, when jobs are launched
664 they will report that they have started but then pause until
665 released before reporting completion. This attribute may be
666 changed at any time and will take effect for subsequently
667 launched builds, but previously held builds will still need to
668 be explicitly released.
669
670 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800671 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700672 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800673 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700674 self.hold_jobs_in_build = False
675 self.lock = threading.Lock()
676 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700677 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700678 self._build_counter_lock = threading.Lock()
679 self.build_counter = 0
680 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700681 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800682
James E. Blaire1767bc2016-08-02 10:00:27 -0700683 def addFailTest(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700684 """Instruct the launcher to report matching builds as failures.
685
686 :arg str name: The name of the job to fail.
687 :arg change: TODO: document
688
689 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700690 l = self.fail_tests.get(name, [])
691 l.append(change)
692 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800693
James E. Blaire1767bc2016-08-02 10:00:27 -0700694 def shouldFailTest(self, name, ref):
695 l = self.fail_tests.get(name, [])
696 for change in l:
697 if self.test.ref_has_change(ref, change):
698 return True
699 return False
James E. Blairf5dbd002015-12-23 15:26:17 -0800700
James E. Blair962220f2016-08-03 11:22:38 -0700701 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700702 """Release a held build.
703
704 :arg str regex: A regular expression which, if supplied, will
705 cause only builds with matching names to be released. If
706 not supplied, all builds will be released.
707
708 """
James E. Blair962220f2016-08-03 11:22:38 -0700709 builds = self.running_builds[:]
710 self.log.debug("Releasing build %s (%s)" % (regex,
711 len(self.running_builds)))
712 for build in builds:
713 if not regex or re.match(regex, build.name):
714 self.log.debug("Releasing build %s" %
715 (build.parameters['ZUUL_UUID']))
716 build.release()
717 else:
718 self.log.debug("Not releasing build %s" %
719 (build.parameters['ZUUL_UUID']))
720 self.log.debug("Done releasing builds %s (%s)" %
721 (regex, len(self.running_builds)))
722
James E. Blairab7132b2016-08-05 12:36:22 -0700723 def launch(self, job):
James E. Blaire1767bc2016-08-02 10:00:27 -0700724 with self._build_counter_lock:
725 self.build_counter += 1
726 build_counter = self.build_counter
727 node = None
James E. Blairab7132b2016-08-05 12:36:22 -0700728 build = FakeBuild(self, job, build_counter, node)
James E. Blaire1767bc2016-08-02 10:00:27 -0700729 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700730 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700731 self.job_builds[job.unique] = build
732 super(RecordingLaunchServer, self).launch(job)
733
734 def runAnsible(self, jobdir, job):
735 build = self.job_builds[job.unique]
736 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700737
738 if self._run_ansible:
739 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
740 else:
741 result = build.run()
742
743 self.lock.acquire()
744 self.build_history.append(
745 BuildHistory(name=build.name, number=build.number,
746 result=result, changes=build.changes, node=build.node,
747 uuid=build.unique, parameters=build.parameters,
748 pipeline=build.parameters['ZUUL_PIPELINE'])
749 )
James E. Blairab7132b2016-08-05 12:36:22 -0700750 self.running_builds.remove(build)
751 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700752 self.lock.release()
753 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800754
755
Clark Boylanb640e052014-04-03 16:41:46 -0700756class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700757 """A Gearman server for use in tests.
758
759 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
760 added to the queue but will not be distributed to workers
761 until released. This attribute may be changed at any time and
762 will take effect for subsequently enqueued jobs, but
763 previously held jobs will still need to be explicitly
764 released.
765
766 """
767
Clark Boylanb640e052014-04-03 16:41:46 -0700768 def __init__(self):
769 self.hold_jobs_in_queue = False
770 super(FakeGearmanServer, self).__init__(0)
771
772 def getJobForConnection(self, connection, peek=False):
773 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
774 for job in queue:
775 if not hasattr(job, 'waiting'):
776 if job.name.startswith('build:'):
777 job.waiting = self.hold_jobs_in_queue
778 else:
779 job.waiting = False
780 if job.waiting:
781 continue
782 if job.name in connection.functions:
783 if not peek:
784 queue.remove(job)
785 connection.related_jobs[job.handle] = job
786 job.worker_connection = connection
787 job.running = True
788 return job
789 return None
790
791 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700792 """Release a held job.
793
794 :arg str regex: A regular expression which, if supplied, will
795 cause only jobs with matching names to be released. If
796 not supplied, all jobs will be released.
797 """
Clark Boylanb640e052014-04-03 16:41:46 -0700798 released = False
799 qlen = (len(self.high_queue) + len(self.normal_queue) +
800 len(self.low_queue))
801 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
802 for job in self.getQueue():
803 cmd, name = job.name.split(':')
804 if cmd != 'build':
805 continue
806 if not regex or re.match(regex, name):
807 self.log.debug("releasing queued job %s" %
808 job.unique)
809 job.waiting = False
810 released = True
811 else:
812 self.log.debug("not releasing queued job %s" %
813 job.unique)
814 if released:
815 self.wakeConnections()
816 qlen = (len(self.high_queue) + len(self.normal_queue) +
817 len(self.low_queue))
818 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
819
820
821class FakeSMTP(object):
822 log = logging.getLogger('zuul.FakeSMTP')
823
824 def __init__(self, messages, server, port):
825 self.server = server
826 self.port = port
827 self.messages = messages
828
829 def sendmail(self, from_email, to_email, msg):
830 self.log.info("Sending email from %s, to %s, with msg %s" % (
831 from_email, to_email, msg))
832
833 headers = msg.split('\n\n', 1)[0]
834 body = msg.split('\n\n', 1)[1]
835
836 self.messages.append(dict(
837 from_email=from_email,
838 to_email=to_email,
839 msg=msg,
840 headers=headers,
841 body=body,
842 ))
843
844 return True
845
846 def quit(self):
847 return True
848
849
850class FakeSwiftClientConnection(swiftclient.client.Connection):
851 def post_account(self, headers):
852 # Do nothing
853 pass
854
855 def get_auth(self):
856 # Returns endpoint and (unused) auth token
857 endpoint = os.path.join('https://storage.example.org', 'V1',
858 'AUTH_account')
859 return endpoint, ''
860
861
Maru Newby3fe5f852015-01-13 04:22:14 +0000862class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700863 log = logging.getLogger("zuul.test")
864
865 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000866 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700867 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
868 try:
869 test_timeout = int(test_timeout)
870 except ValueError:
871 # If timeout value is invalid do not set a timeout.
872 test_timeout = 0
873 if test_timeout > 0:
874 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
875
876 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
877 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
878 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
879 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
880 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
881 os.environ.get('OS_STDERR_CAPTURE') == '1'):
882 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
883 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
884 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
885 os.environ.get('OS_LOG_CAPTURE') == '1'):
886 self.useFixture(fixtures.FakeLogger(
887 level=logging.DEBUG,
888 format='%(asctime)s %(name)-32s '
889 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000890
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700891 # NOTE(notmorgan): Extract logging overrides for specific libraries
892 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
893 # each. This is used to limit the output during test runs from
894 # libraries that zuul depends on such as gear.
895 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
896
897 if log_defaults_from_env:
898 for default in log_defaults_from_env.split(','):
899 try:
900 name, level_str = default.split('=', 1)
901 level = getattr(logging, level_str, logging.DEBUG)
902 self.useFixture(fixtures.FakeLogger(
903 name=name,
904 level=level,
905 format='%(asctime)s %(name)-32s '
906 '%(levelname)-8s %(message)s'))
907 except ValueError:
908 # NOTE(notmorgan): Invalid format of the log default,
909 # skip and don't try and apply a logger for the
910 # specified module
911 pass
912
Maru Newby3fe5f852015-01-13 04:22:14 +0000913
914class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700915 """A test case with a functioning Zuul.
916
917 The following class variables are used during test setup and can
918 be overidden by subclasses but are effectively read-only once a
919 test method starts running:
920
921 :cvar str config_file: This points to the main zuul config file
922 within the fixtures directory. Subclasses may override this
923 to obtain a different behavior.
924
925 :cvar str tenant_config_file: This is the tenant config file
926 (which specifies from what git repos the configuration should
927 be loaded). It defaults to the value specified in
928 `config_file` but can be overidden by subclasses to obtain a
929 different tenant/project layout while using the standard main
930 configuration.
931
932 The following are instance variables that are useful within test
933 methods:
934
935 :ivar FakeGerritConnection fake_<connection>:
936 A :py:class:`~tests.base.FakeGerritConnection` will be
937 instantiated for each connection present in the config file
938 and stored here. For instance, `fake_gerrit` will hold the
939 FakeGerritConnection object for a connection named `gerrit`.
940
941 :ivar FakeGearmanServer gearman_server: An instance of
942 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
943 server that all of the Zuul components in this test use to
944 communicate with each other.
945
946 :ivar RecordingLaunchServer launch_server: An instance of
947 :py:class:`~tests.base.RecordingLaunchServer` which is the
948 Ansible launch server used to run jobs for this test.
949
950 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
951 representing currently running builds. They are appended to
952 the list in the order they are launched, and removed from this
953 list upon completion.
954
955 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
956 objects representing completed builds. They are appended to
957 the list in the order they complete.
958
959 """
960
James E. Blair83005782015-12-11 14:46:03 -0800961 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700962 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700963
964 def _startMerger(self):
965 self.merge_server = zuul.merger.server.MergeServer(self.config,
966 self.connections)
967 self.merge_server.start()
968
Maru Newby3fe5f852015-01-13 04:22:14 +0000969 def setUp(self):
970 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700971 if USE_TEMPDIR:
972 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000973 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
974 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700975 else:
976 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700977 self.test_root = os.path.join(tmp_root, "zuul-test")
978 self.upstream_root = os.path.join(self.test_root, "upstream")
979 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700980 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700981
982 if os.path.exists(self.test_root):
983 shutil.rmtree(self.test_root)
984 os.makedirs(self.test_root)
985 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700986 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700987
988 # Make per test copy of Configuration.
989 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800990 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100991 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800992 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700993 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700994 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700995
996 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700997 # TODOv3(jeblair): remove these and replace with new git
998 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -0700999 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001000 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001001 self.init_repo("org/project5")
1002 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001003 self.init_repo("org/one-job-project")
1004 self.init_repo("org/nonvoting-project")
1005 self.init_repo("org/templated-project")
1006 self.init_repo("org/layered-project")
1007 self.init_repo("org/node-project")
1008 self.init_repo("org/conflict-project")
1009 self.init_repo("org/noop-project")
1010 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001011 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001012
1013 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001014 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1015 # see: https://github.com/jsocol/pystatsd/issues/61
1016 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001017 os.environ['STATSD_PORT'] = str(self.statsd.port)
1018 self.statsd.start()
1019 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001020 reload_module(statsd)
1021 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001022
1023 self.gearman_server = FakeGearmanServer()
1024
1025 self.config.set('gearman', 'port', str(self.gearman_server.port))
1026
Joshua Hesketh352264b2015-08-11 23:42:08 +10001027 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1028 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1029 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001030
Joshua Hesketh352264b2015-08-11 23:42:08 +10001031 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001032
1033 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1034 FakeSwiftClientConnection))
1035 self.swift = zuul.lib.swift.Swift(self.config)
1036
Jan Hruban6b71aff2015-10-22 16:58:08 +02001037 self.event_queues = [
1038 self.sched.result_event_queue,
1039 self.sched.trigger_event_queue
1040 ]
1041
James E. Blairfef78942016-03-11 16:28:56 -08001042 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001043 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001044
Clark Boylanb640e052014-04-03 16:41:46 -07001045 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001046 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001047 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001048 return FakeURLOpener(self.upstream_root, *args, **kw)
1049
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001050 old_urlopen = urllib.request.urlopen
1051 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001052
James E. Blair3f876d52016-07-22 13:07:14 -07001053 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001054
James E. Blaire1767bc2016-08-02 10:00:27 -07001055 self.launch_server = RecordingLaunchServer(
1056 self.config, self.connections, _run_ansible=self.run_ansible)
1057 self.launch_server.start()
1058 self.history = self.launch_server.build_history
1059 self.builds = self.launch_server.running_builds
1060
1061 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001062 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001063 self.merge_client = zuul.merger.client.MergeClient(
1064 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001065 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001066
James E. Blaire1767bc2016-08-02 10:00:27 -07001067 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001068 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001069 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001070
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001071 self.webapp = zuul.webapp.WebApp(
1072 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001073 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001074
1075 self.sched.start()
1076 self.sched.reconfigure(self.config)
1077 self.sched.resume()
1078 self.webapp.start()
1079 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001080 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001081
1082 self.addCleanup(self.assertFinalState)
1083 self.addCleanup(self.shutdown)
1084
James E. Blairfef78942016-03-11 16:28:56 -08001085 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001086 # Register connections from the config
1087 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001088
Joshua Hesketh352264b2015-08-11 23:42:08 +10001089 def FakeSMTPFactory(*args, **kw):
1090 args = [self.smtp_messages] + list(args)
1091 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001092
Joshua Hesketh352264b2015-08-11 23:42:08 +10001093 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001094
Joshua Hesketh352264b2015-08-11 23:42:08 +10001095 # Set a changes database so multiple FakeGerrit's can report back to
1096 # a virtual canonical database given by the configured hostname
1097 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001098 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001099
Joshua Hesketh352264b2015-08-11 23:42:08 +10001100 for section_name in self.config.sections():
1101 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1102 section_name, re.I)
1103 if not con_match:
1104 continue
1105 con_name = con_match.group(2)
1106 con_config = dict(self.config.items(section_name))
1107
1108 if 'driver' not in con_config:
1109 raise Exception("No driver specified for connection %s."
1110 % con_name)
1111
1112 con_driver = con_config['driver']
1113
1114 # TODO(jhesketh): load the required class automatically
1115 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001116 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1117 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001118 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001119 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001120 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001121 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001122 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001123 self.event_queues.append(
1124 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001125 setattr(self, 'fake_' + con_name,
1126 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001127 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001128 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001129 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1130 else:
1131 raise Exception("Unknown driver, %s, for connection %s"
1132 % (con_config['driver'], con_name))
1133
1134 # If the [gerrit] or [smtp] sections still exist, load them in as a
1135 # connection named 'gerrit' or 'smtp' respectfully
1136
1137 if 'gerrit' in self.config.sections():
1138 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001139 self.event_queues.append(
1140 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001141 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001142 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001143 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001144
1145 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001146 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001147 zuul.connection.smtp.SMTPConnection(
1148 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001149
James E. Blair83005782015-12-11 14:46:03 -08001150 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001151 # This creates the per-test configuration object. It can be
1152 # overriden by subclasses, but should not need to be since it
1153 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001154 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001155 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001156 if hasattr(self, 'tenant_config_file'):
1157 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001158 git_path = os.path.join(
1159 os.path.dirname(
1160 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1161 'git')
1162 if os.path.exists(git_path):
1163 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001164 project = reponame.replace('_', '/')
1165 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001166 os.path.join(git_path, reponame))
1167
1168 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001169 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001170
1171 files = {}
1172 for (dirpath, dirnames, filenames) in os.walk(source_path):
1173 for filename in filenames:
1174 test_tree_filepath = os.path.join(dirpath, filename)
1175 common_path = os.path.commonprefix([test_tree_filepath,
1176 source_path])
1177 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1178 with open(test_tree_filepath, 'r') as f:
1179 content = f.read()
1180 files[relative_filepath] = content
1181 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001182 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001183
Clark Boylanb640e052014-04-03 16:41:46 -07001184 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001185 # Make sure that git.Repo objects have been garbage collected.
1186 repos = []
1187 gc.collect()
1188 for obj in gc.get_objects():
1189 if isinstance(obj, git.Repo):
1190 repos.append(obj)
1191 self.assertEqual(len(repos), 0)
1192 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001193 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001194 for tenant in self.sched.abide.tenants.values():
1195 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001196 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001197 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001198
1199 def shutdown(self):
1200 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001201 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001202 self.merge_server.stop()
1203 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001204 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001205 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001206 self.sched.stop()
1207 self.sched.join()
1208 self.statsd.stop()
1209 self.statsd.join()
1210 self.webapp.stop()
1211 self.webapp.join()
1212 self.rpc.stop()
1213 self.rpc.join()
1214 self.gearman_server.shutdown()
1215 threads = threading.enumerate()
1216 if len(threads) > 1:
1217 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001218
1219 def init_repo(self, project):
1220 parts = project.split('/')
1221 path = os.path.join(self.upstream_root, *parts[:-1])
1222 if not os.path.exists(path):
1223 os.makedirs(path)
1224 path = os.path.join(self.upstream_root, project)
1225 repo = git.Repo.init(path)
1226
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001227 with repo.config_writer() as config_writer:
1228 config_writer.set_value('user', 'email', 'user@example.com')
1229 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001230
Clark Boylanb640e052014-04-03 16:41:46 -07001231 repo.index.commit('initial commit')
1232 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001233
James E. Blair97d902e2014-08-21 13:25:56 -07001234 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001235 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001236 repo.git.clean('-x', '-f', '-d')
1237
James E. Blair97d902e2014-08-21 13:25:56 -07001238 def create_branch(self, project, branch):
1239 path = os.path.join(self.upstream_root, project)
1240 repo = git.Repo.init(path)
1241 fn = os.path.join(path, 'README')
1242
1243 branch_head = repo.create_head(branch)
1244 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001245 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001246 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001247 f.close()
1248 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001249 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001250
James E. Blair97d902e2014-08-21 13:25:56 -07001251 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001252 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001253 repo.git.clean('-x', '-f', '-d')
1254
Sachi King9f16d522016-03-16 12:20:45 +11001255 def create_commit(self, project):
1256 path = os.path.join(self.upstream_root, project)
1257 repo = git.Repo(path)
1258 repo.head.reference = repo.heads['master']
1259 file_name = os.path.join(path, 'README')
1260 with open(file_name, 'a') as f:
1261 f.write('creating fake commit\n')
1262 repo.index.add([file_name])
1263 commit = repo.index.commit('Creating a fake commit')
1264 return commit.hexsha
1265
Clark Boylanb640e052014-04-03 16:41:46 -07001266 def ref_has_change(self, ref, change):
1267 path = os.path.join(self.git_root, change.project)
1268 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001269 try:
1270 for commit in repo.iter_commits(ref):
1271 if commit.message.strip() == ('%s-1' % change.subject):
1272 return True
1273 except GitCommandError:
1274 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001275 return False
1276
James E. Blairb8c16472015-05-05 14:55:26 -07001277 def orderedRelease(self):
1278 # Run one build at a time to ensure non-race order:
1279 while len(self.builds):
1280 self.release(self.builds[0])
1281 self.waitUntilSettled()
1282
Clark Boylanb640e052014-04-03 16:41:46 -07001283 def release(self, job):
1284 if isinstance(job, FakeBuild):
1285 job.release()
1286 else:
1287 job.waiting = False
1288 self.log.debug("Queued job %s released" % job.unique)
1289 self.gearman_server.wakeConnections()
1290
1291 def getParameter(self, job, name):
1292 if isinstance(job, FakeBuild):
1293 return job.parameters[name]
1294 else:
1295 parameters = json.loads(job.arguments)
1296 return parameters[name]
1297
1298 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001299 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001300 while True:
1301 done = True
1302 for connection in self.gearman_server.active_connections:
1303 if (connection.functions and
1304 connection.client_id not in ['Zuul RPC Listener',
1305 'Zuul Merger']):
1306 done = False
1307 if done:
1308 break
1309 time.sleep(0)
1310 self.gearman_server.functions = set()
1311 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001312
1313 def haveAllBuildsReported(self):
1314 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001315 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001316 return False
1317 # Find out if every build that the worker has completed has been
1318 # reported back to Zuul. If it hasn't then that means a Gearman
1319 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001320 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001321 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001322 if not zbuild:
1323 # It has already been reported
1324 continue
1325 # It hasn't been reported yet.
1326 return False
1327 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001328 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001329 if connection.state == 'GRAB_WAIT':
1330 return False
1331 return True
1332
1333 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001334 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001335 for build in builds:
1336 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001337 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001338 for j in conn.related_jobs.values():
1339 if j.unique == build.uuid:
1340 client_job = j
1341 break
1342 if not client_job:
1343 self.log.debug("%s is not known to the gearman client" %
1344 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001345 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001346 if not client_job.handle:
1347 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001348 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001349 server_job = self.gearman_server.jobs.get(client_job.handle)
1350 if not server_job:
1351 self.log.debug("%s is not known to the gearman server" %
1352 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001353 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001354 if not hasattr(server_job, 'waiting'):
1355 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001356 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001357 if server_job.waiting:
1358 continue
James E. Blairbbda4702016-03-09 15:19:56 -08001359 if build.number is None:
1360 self.log.debug("%s has not reported start" % build)
1361 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001362 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001363 if worker_build:
1364 if worker_build.isWaiting():
1365 continue
1366 else:
1367 self.log.debug("%s is running" % worker_build)
1368 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001369 else:
James E. Blair962220f2016-08-03 11:22:38 -07001370 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001371 return False
1372 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001373
Jan Hruban6b71aff2015-10-22 16:58:08 +02001374 def eventQueuesEmpty(self):
1375 for queue in self.event_queues:
1376 yield queue.empty()
1377
1378 def eventQueuesJoin(self):
1379 for queue in self.event_queues:
1380 queue.join()
1381
Clark Boylanb640e052014-04-03 16:41:46 -07001382 def waitUntilSettled(self):
1383 self.log.debug("Waiting until settled...")
1384 start = time.time()
1385 while True:
1386 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001387 self.log.debug("Queue status:")
1388 for queue in self.event_queues:
1389 self.log.debug(" %s: %s" % (queue, queue.empty()))
1390 self.log.debug("All builds waiting: %s" %
1391 (self.areAllBuildsWaiting(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001392 raise Exception("Timeout waiting for Zuul to settle")
1393 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001394
James E. Blaire1767bc2016-08-02 10:00:27 -07001395 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001396 # have all build states propogated to zuul?
1397 if self.haveAllBuildsReported():
1398 # Join ensures that the queue is empty _and_ events have been
1399 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001400 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001402 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001403 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001404 self.haveAllBuildsReported() and
1405 self.areAllBuildsWaiting()):
1406 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001407 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001408 self.log.debug("...settled.")
1409 return
1410 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001411 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001412 self.sched.wake_event.wait(0.1)
1413
1414 def countJobResults(self, jobs, result):
1415 jobs = filter(lambda x: x.result == result, jobs)
1416 return len(jobs)
1417
James E. Blair96c6bf82016-01-15 16:20:40 -08001418 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001419 for job in self.history:
1420 if (job.name == name and
1421 (project is None or
1422 job.parameters['ZUUL_PROJECT'] == project)):
1423 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001424 raise Exception("Unable to find job %s in history" % name)
1425
1426 def assertEmptyQueues(self):
1427 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001428 for tenant in self.sched.abide.tenants.values():
1429 for pipeline in tenant.layout.pipelines.values():
1430 for queue in pipeline.queues:
1431 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001432 print('pipeline %s queue %s contents %s' % (
1433 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001434 self.assertEqual(len(queue.queue), 0,
1435 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001436
1437 def assertReportedStat(self, key, value=None, kind=None):
1438 start = time.time()
1439 while time.time() < (start + 5):
1440 for stat in self.statsd.stats:
1441 pprint.pprint(self.statsd.stats)
1442 k, v = stat.split(':')
1443 if key == k:
1444 if value is None and kind is None:
1445 return
1446 elif value:
1447 if value == v:
1448 return
1449 elif kind:
1450 if v.endswith('|' + kind):
1451 return
1452 time.sleep(0.1)
1453
1454 pprint.pprint(self.statsd.stats)
1455 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001456
1457 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001458 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1459
1460 def updateConfigLayout(self, path):
1461 root = os.path.join(self.test_root, "config")
1462 os.makedirs(root)
1463 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1464 f.write("""
1465tenants:
1466 - name: openstack
1467 include:
1468 - %s
1469 """ % os.path.abspath(path))
1470 f.close()
1471 self.config.set('zuul', 'tenant_config', f.name)
James E. Blair14abdf42015-12-09 16:11:53 -08001472
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001473 def addCommitToRepo(self, project, message, files,
1474 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001475 path = os.path.join(self.upstream_root, project)
1476 repo = git.Repo(path)
1477 repo.head.reference = branch
1478 zuul.merger.merger.reset_repo_to_head(repo)
1479 for fn, content in files.items():
1480 fn = os.path.join(path, fn)
1481 with open(fn, 'w') as f:
1482 f.write(content)
1483 repo.index.add([fn])
1484 commit = repo.index.commit(message)
1485 repo.heads[branch].commit = commit
1486 repo.head.reference = branch
1487 repo.git.clean('-x', '-f', '-d')
1488 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001489 if tag:
1490 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001491
James E. Blair7fc8daa2016-08-08 15:37:15 -07001492 def addEvent(self, connection, event):
1493 """Inject a Fake (Gerrit) event.
1494
1495 This method accepts a JSON-encoded event and simulates Zuul
1496 having received it from Gerrit. It could (and should)
1497 eventually apply to any connection type, but is currently only
1498 used with Gerrit connections. The name of the connection is
1499 used to look up the corresponding server, and the event is
1500 simulated as having been received by all Zuul connections
1501 attached to that server. So if two Gerrit connections in Zuul
1502 are connected to the same Gerrit server, and you invoke this
1503 method specifying the name of one of them, the event will be
1504 received by both.
1505
1506 .. note::
1507
1508 "self.fake_gerrit.addEvent" calls should be migrated to
1509 this method.
1510
1511 :arg str connection: The name of the connection corresponding
1512 to the gerrit server.
1513 :arg str event: The JSON-encoded event.
1514
1515 """
1516 specified_conn = self.connections.connections[connection]
1517 for conn in self.connections.connections.values():
1518 if (isinstance(conn, specified_conn.__class__) and
1519 specified_conn.server == conn.server):
1520 conn.addEvent(event)
1521
James E. Blair3f876d52016-07-22 13:07:14 -07001522
1523class AnsibleZuulTestCase(ZuulTestCase):
1524 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001525 run_ansible = True