blob: 9845484ca0dcd9e9c3d01f3f0841b4ef2987f5cb [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
24import pprint
Christian Berendt12d4d722014-06-07 21:03:45 +020025from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070026from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070027import random
28import re
29import select
30import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030031from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070032import socket
33import string
34import subprocess
35import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
38import time
Clark Boylanb640e052014-04-03 16:41:46 -070039
40import git
41import gear
42import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080043import kazoo.client
Clark Boylanb640e052014-04-03 16:41:46 -070044import statsd
45import testtools
Clint Byrum3343e3e2016-11-15 16:05:03 -080046from git.exc import NoSuchPathError
Clark Boylanb640e052014-04-03 16:41:46 -070047
Joshua Hesketh352264b2015-08-11 23:42:08 +100048import zuul.connection.gerrit
49import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070050import zuul.scheduler
51import zuul.webapp
52import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100053import zuul.launcher.server
54import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080056import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070057import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070058import zuul.merger.merger
59import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070060import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070061import zuul.reporter.gerrit
62import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110063import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.trigger.gerrit
65import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070066import zuul.trigger.zuultrigger
Clark Boylanb640e052014-04-03 16:41:46 -070067
68FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
69 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070070USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070071
72logging.basicConfig(level=logging.DEBUG,
73 format='%(asctime)s %(name)-32s '
74 '%(levelname)-8s %(message)s')
75
76
77def repack_repo(path):
78 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
79 output = subprocess.Popen(cmd, close_fds=True,
80 stdout=subprocess.PIPE,
81 stderr=subprocess.PIPE)
82 out = output.communicate()
83 if output.returncode:
84 raise Exception("git repack returned %d" % output.returncode)
85 return out
86
87
88def random_sha1():
89 return hashlib.sha1(str(random.random())).hexdigest()
90
91
James E. Blaira190f3b2015-01-05 14:56:54 -080092def iterate_timeout(max_seconds, purpose):
93 start = time.time()
94 count = 0
95 while (time.time() < start + max_seconds):
96 count += 1
97 yield count
98 time.sleep(0)
99 raise Exception("Timeout waiting for %s" % purpose)
100
101
Clark Boylanb640e052014-04-03 16:41:46 -0700102class ChangeReference(git.Reference):
103 _common_path_default = "refs/changes"
104 _points_to_commits_only = True
105
106
107class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700108 categories = {'approved': ('Approved', -1, 1),
109 'code-review': ('Code-Review', -2, 2),
110 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700111
112 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700113 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700114 self.gerrit = gerrit
115 self.reported = 0
116 self.queried = 0
117 self.patchsets = []
118 self.number = number
119 self.project = project
120 self.branch = branch
121 self.subject = subject
122 self.latest_patchset = 0
123 self.depends_on_change = None
124 self.needed_by_changes = []
125 self.fail_merge = False
126 self.messages = []
127 self.data = {
128 'branch': branch,
129 'comments': [],
130 'commitMessage': subject,
131 'createdOn': time.time(),
132 'id': 'I' + random_sha1(),
133 'lastUpdated': time.time(),
134 'number': str(number),
135 'open': status == 'NEW',
136 'owner': {'email': 'user@example.com',
137 'name': 'User Name',
138 'username': 'username'},
139 'patchSets': self.patchsets,
140 'project': project,
141 'status': status,
142 'subject': subject,
143 'submitRecords': [],
144 'url': 'https://hostname/%s' % number}
145
146 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700147 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700148 self.data['submitRecords'] = self.getSubmitRecords()
149 self.open = status == 'NEW'
150
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700151 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700152 path = os.path.join(self.upstream_root, self.project)
153 repo = git.Repo(path)
154 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
155 self.latest_patchset),
156 'refs/tags/init')
157 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700158 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700159 repo.git.clean('-x', '-f', '-d')
160
161 path = os.path.join(self.upstream_root, self.project)
162 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700163 for fn, content in files.items():
164 fn = os.path.join(path, fn)
165 with open(fn, 'w') as f:
166 f.write(content)
167 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700168 else:
169 for fni in range(100):
170 fn = os.path.join(path, str(fni))
171 f = open(fn, 'w')
172 for ci in range(4096):
173 f.write(random.choice(string.printable))
174 f.close()
175 repo.index.add([fn])
176
177 r = repo.index.commit(msg)
178 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700179 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700180 repo.git.clean('-x', '-f', '-d')
181 repo.heads['master'].checkout()
182 return r
183
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700185 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700187 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700188 data = ("test %s %s %s\n" %
189 (self.branch, self.number, self.latest_patchset))
190 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700191 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700193 ps_files = [{'file': '/COMMIT_MSG',
194 'type': 'ADDED'},
195 {'file': 'README',
196 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700197 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700198 ps_files.append({'file': f, 'type': 'ADDED'})
199 d = {'approvals': [],
200 'createdOn': time.time(),
201 'files': ps_files,
202 'number': str(self.latest_patchset),
203 'ref': 'refs/changes/1/%s/%s' % (self.number,
204 self.latest_patchset),
205 'revision': c.hexsha,
206 'uploader': {'email': 'user@example.com',
207 'name': 'User name',
208 'username': 'user'}}
209 self.data['currentPatchSet'] = d
210 self.patchsets.append(d)
211 self.data['submitRecords'] = self.getSubmitRecords()
212
213 def getPatchsetCreatedEvent(self, patchset):
214 event = {"type": "patchset-created",
215 "change": {"project": self.project,
216 "branch": self.branch,
217 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
218 "number": str(self.number),
219 "subject": self.subject,
220 "owner": {"name": "User Name"},
221 "url": "https://hostname/3"},
222 "patchSet": self.patchsets[patchset - 1],
223 "uploader": {"name": "User Name"}}
224 return event
225
226 def getChangeRestoredEvent(self):
227 event = {"type": "change-restored",
228 "change": {"project": self.project,
229 "branch": self.branch,
230 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
231 "number": str(self.number),
232 "subject": self.subject,
233 "owner": {"name": "User Name"},
234 "url": "https://hostname/3"},
235 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100236 "patchSet": self.patchsets[-1],
237 "reason": ""}
238 return event
239
240 def getChangeAbandonedEvent(self):
241 event = {"type": "change-abandoned",
242 "change": {"project": self.project,
243 "branch": self.branch,
244 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
245 "number": str(self.number),
246 "subject": self.subject,
247 "owner": {"name": "User Name"},
248 "url": "https://hostname/3"},
249 "abandoner": {"name": "User Name"},
250 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700251 "reason": ""}
252 return event
253
254 def getChangeCommentEvent(self, patchset):
255 event = {"type": "comment-added",
256 "change": {"project": self.project,
257 "branch": self.branch,
258 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
259 "number": str(self.number),
260 "subject": self.subject,
261 "owner": {"name": "User Name"},
262 "url": "https://hostname/3"},
263 "patchSet": self.patchsets[patchset - 1],
264 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700265 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700266 "description": "Code-Review",
267 "value": "0"}],
268 "comment": "This is a comment"}
269 return event
270
Joshua Hesketh642824b2014-07-01 17:54:59 +1000271 def addApproval(self, category, value, username='reviewer_john',
272 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700273 if not granted_on:
274 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000275 approval = {
276 'description': self.categories[category][0],
277 'type': category,
278 'value': str(value),
279 'by': {
280 'username': username,
281 'email': username + '@example.com',
282 },
283 'grantedOn': int(granted_on)
284 }
Clark Boylanb640e052014-04-03 16:41:46 -0700285 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
286 if x['by']['username'] == username and x['type'] == category:
287 del self.patchsets[-1]['approvals'][i]
288 self.patchsets[-1]['approvals'].append(approval)
289 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000290 'author': {'email': 'author@example.com',
291 'name': 'Patchset Author',
292 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700293 'change': {'branch': self.branch,
294 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
295 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000296 'owner': {'email': 'owner@example.com',
297 'name': 'Change Owner',
298 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700299 'project': self.project,
300 'subject': self.subject,
301 'topic': 'master',
302 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000303 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700304 'patchSet': self.patchsets[-1],
305 'type': 'comment-added'}
306 self.data['submitRecords'] = self.getSubmitRecords()
307 return json.loads(json.dumps(event))
308
309 def getSubmitRecords(self):
310 status = {}
311 for cat in self.categories.keys():
312 status[cat] = 0
313
314 for a in self.patchsets[-1]['approvals']:
315 cur = status[a['type']]
316 cat_min, cat_max = self.categories[a['type']][1:]
317 new = int(a['value'])
318 if new == cat_min:
319 cur = new
320 elif abs(new) > abs(cur):
321 cur = new
322 status[a['type']] = cur
323
324 labels = []
325 ok = True
326 for typ, cat in self.categories.items():
327 cur = status[typ]
328 cat_min, cat_max = cat[1:]
329 if cur == cat_min:
330 value = 'REJECT'
331 ok = False
332 elif cur == cat_max:
333 value = 'OK'
334 else:
335 value = 'NEED'
336 ok = False
337 labels.append({'label': cat[0], 'status': value})
338 if ok:
339 return [{'status': 'OK'}]
340 return [{'status': 'NOT_READY',
341 'labels': labels}]
342
343 def setDependsOn(self, other, patchset):
344 self.depends_on_change = other
345 d = {'id': other.data['id'],
346 'number': other.data['number'],
347 'ref': other.patchsets[patchset - 1]['ref']
348 }
349 self.data['dependsOn'] = [d]
350
351 other.needed_by_changes.append(self)
352 needed = other.data.get('neededBy', [])
353 d = {'id': self.data['id'],
354 'number': self.data['number'],
355 'ref': self.patchsets[patchset - 1]['ref'],
356 'revision': self.patchsets[patchset - 1]['revision']
357 }
358 needed.append(d)
359 other.data['neededBy'] = needed
360
361 def query(self):
362 self.queried += 1
363 d = self.data.get('dependsOn')
364 if d:
365 d = d[0]
366 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
367 d['isCurrentPatchSet'] = True
368 else:
369 d['isCurrentPatchSet'] = False
370 return json.loads(json.dumps(self.data))
371
372 def setMerged(self):
373 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000374 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700375 return
376 if self.fail_merge:
377 return
378 self.data['status'] = 'MERGED'
379 self.open = False
380
381 path = os.path.join(self.upstream_root, self.project)
382 repo = git.Repo(path)
383 repo.heads[self.branch].commit = \
384 repo.commit(self.patchsets[-1]['revision'])
385
386 def setReported(self):
387 self.reported += 1
388
389
Joshua Hesketh352264b2015-08-11 23:42:08 +1000390class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700391 """A Fake Gerrit connection for use in tests.
392
393 This subclasses
394 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
395 ability for tests to add changes to the fake Gerrit it represents.
396 """
397
Joshua Hesketh352264b2015-08-11 23:42:08 +1000398 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700399
Joshua Hesketh352264b2015-08-11 23:42:08 +1000400 def __init__(self, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700401 changes_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000402 super(FakeGerritConnection, self).__init__(connection_name,
403 connection_config)
404
James E. Blair7fc8daa2016-08-08 15:37:15 -0700405 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700406 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
407 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000408 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700409 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200410 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700411
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700412 def addFakeChange(self, project, branch, subject, status='NEW',
413 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700414 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700415 self.change_number += 1
416 c = FakeChange(self, self.change_number, project, branch, subject,
417 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700418 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700419 self.changes[self.change_number] = c
420 return c
421
Clark Boylanb640e052014-04-03 16:41:46 -0700422 def review(self, project, changeid, message, action):
423 number, ps = changeid.split(',')
424 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000425
426 # Add the approval back onto the change (ie simulate what gerrit would
427 # do).
428 # Usually when zuul leaves a review it'll create a feedback loop where
429 # zuul's review enters another gerrit event (which is then picked up by
430 # zuul). However, we can't mimic this behaviour (by adding this
431 # approval event into the queue) as it stops jobs from checking what
432 # happens before this event is triggered. If a job needs to see what
433 # happens they can add their own verified event into the queue.
434 # Nevertheless, we can update change with the new review in gerrit.
435
James E. Blair8b5408c2016-08-08 15:37:46 -0700436 for cat in action.keys():
437 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000438 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000439
James E. Blair8b5408c2016-08-08 15:37:46 -0700440 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000441 if 'label' in action:
442 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000443 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000444
Clark Boylanb640e052014-04-03 16:41:46 -0700445 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000446
Clark Boylanb640e052014-04-03 16:41:46 -0700447 if 'submit' in action:
448 change.setMerged()
449 if message:
450 change.setReported()
451
452 def query(self, number):
453 change = self.changes.get(int(number))
454 if change:
455 return change.query()
456 return {}
457
James E. Blairc494d542014-08-06 09:23:52 -0700458 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700459 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700460 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800461 if query.startswith('change:'):
462 # Query a specific changeid
463 changeid = query[len('change:'):]
464 l = [change.query() for change in self.changes.values()
465 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700466 elif query.startswith('message:'):
467 # Query the content of a commit message
468 msg = query[len('message:'):].strip()
469 l = [change.query() for change in self.changes.values()
470 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800471 else:
472 # Query all open changes
473 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700474 return l
James E. Blairc494d542014-08-06 09:23:52 -0700475
Joshua Hesketh352264b2015-08-11 23:42:08 +1000476 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700477 pass
478
Joshua Hesketh352264b2015-08-11 23:42:08 +1000479 def getGitUrl(self, project):
480 return os.path.join(self.upstream_root, project.name)
481
Adam Gandelmanc5e4f1d2016-11-29 14:27:17 -0800482 def _getGitwebUrl(self, project, sha=None):
483 return self.getGitwebUrl(project, sha)
484
Clark Boylanb640e052014-04-03 16:41:46 -0700485
486class BuildHistory(object):
487 def __init__(self, **kw):
488 self.__dict__.update(kw)
489
490 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700491 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
492 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700493
494
495class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200496 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700497 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700498 self.url = url
499
500 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700501 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700502 path = res.path
503 project = '/'.join(path.split('/')[2:-2])
504 ret = '001e# service=git-upload-pack\n'
505 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
506 'multi_ack thin-pack side-band side-band-64k ofs-delta '
507 'shallow no-progress include-tag multi_ack_detailed no-done\n')
508 path = os.path.join(self.upstream_root, project)
509 repo = git.Repo(path)
510 for ref in repo.refs:
511 r = ref.object.hexsha + ' ' + ref.path + '\n'
512 ret += '%04x%s' % (len(r) + 4, r)
513 ret += '0000'
514 return ret
515
516
Clark Boylanb640e052014-04-03 16:41:46 -0700517class FakeStatsd(threading.Thread):
518 def __init__(self):
519 threading.Thread.__init__(self)
520 self.daemon = True
521 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
522 self.sock.bind(('', 0))
523 self.port = self.sock.getsockname()[1]
524 self.wake_read, self.wake_write = os.pipe()
525 self.stats = []
526
527 def run(self):
528 while True:
529 poll = select.poll()
530 poll.register(self.sock, select.POLLIN)
531 poll.register(self.wake_read, select.POLLIN)
532 ret = poll.poll()
533 for (fd, event) in ret:
534 if fd == self.sock.fileno():
535 data = self.sock.recvfrom(1024)
536 if not data:
537 return
538 self.stats.append(data[0])
539 if fd == self.wake_read:
540 return
541
542 def stop(self):
543 os.write(self.wake_write, '1\n')
544
545
James E. Blaire1767bc2016-08-02 10:00:27 -0700546class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700547 log = logging.getLogger("zuul.test")
548
James E. Blair34776ee2016-08-25 13:53:54 -0700549 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700551 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700552 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700553 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700554 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700555 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700556 # TODOv3(jeblair): self.node is really "the image of the node
557 # assigned". We should rename it (self.node_image?) if we
558 # keep using it like this, or we may end up exposing more of
559 # the complexity around multi-node jobs here
560 # (self.nodes[0].image?)
561 self.node = None
562 if len(self.parameters.get('nodes')) == 1:
563 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700564 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700565 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700566 self.wait_condition = threading.Condition()
567 self.waiting = False
568 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500569 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700570 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700571 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700572 self.changes = None
573 if 'ZUUL_CHANGE_IDS' in self.parameters:
574 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700575
James E. Blair3158e282016-08-19 09:34:11 -0700576 def __repr__(self):
577 waiting = ''
578 if self.waiting:
579 waiting = ' [waiting]'
580 return '<FakeBuild %s %s%s>' % (self.name, self.changes, waiting)
581
Clark Boylanb640e052014-04-03 16:41:46 -0700582 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700583 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700584 self.wait_condition.acquire()
585 self.wait_condition.notify()
586 self.waiting = False
587 self.log.debug("Build %s released" % self.unique)
588 self.wait_condition.release()
589
590 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700591 """Return whether this build is being held.
592
593 :returns: Whether the build is being held.
594 :rtype: bool
595 """
596
Clark Boylanb640e052014-04-03 16:41:46 -0700597 self.wait_condition.acquire()
598 if self.waiting:
599 ret = True
600 else:
601 ret = False
602 self.wait_condition.release()
603 return ret
604
605 def _wait(self):
606 self.wait_condition.acquire()
607 self.waiting = True
608 self.log.debug("Build %s waiting" % self.unique)
609 self.wait_condition.wait()
610 self.wait_condition.release()
611
612 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700613 self.log.debug('Running build %s' % self.unique)
614
James E. Blaire1767bc2016-08-02 10:00:27 -0700615 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700616 self.log.debug('Holding build %s' % self.unique)
617 self._wait()
618 self.log.debug("Build %s continuing" % self.unique)
619
Clark Boylanb640e052014-04-03 16:41:46 -0700620 result = 'SUCCESS'
James E. Blaira5dba232016-08-08 15:53:24 -0700621 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
Clark Boylanb640e052014-04-03 16:41:46 -0700622 result = 'FAILURE'
623 if self.aborted:
624 result = 'ABORTED'
Paul Belanger71d98172016-11-08 10:56:31 -0500625 if self.requeue:
626 result = None
Clark Boylanb640e052014-04-03 16:41:46 -0700627
628 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700629 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700630
James E. Blaire1767bc2016-08-02 10:00:27 -0700631 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700632
James E. Blaira5dba232016-08-08 15:53:24 -0700633 def shouldFail(self):
634 changes = self.launch_server.fail_tests.get(self.name, [])
635 for change in changes:
636 if self.hasChanges(change):
637 return True
638 return False
639
James E. Blaire7b99a02016-08-05 14:27:34 -0700640 def hasChanges(self, *changes):
641 """Return whether this build has certain changes in its git repos.
642
643 :arg FakeChange changes: One or more changes (varargs) that
644 are expected to be present (in order) in the git repository of
645 the active project.
646
647 :returns: Whether the build has the indicated changes.
648 :rtype: bool
649
650 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800651 for change in changes:
652 path = os.path.join(self.jobdir.git_root, change.project)
653 try:
654 repo = git.Repo(path)
655 except NoSuchPathError as e:
656 self.log.debug('%s' % e)
657 return False
658 ref = self.parameters['ZUUL_REF']
659 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
660 commit_message = '%s-1' % change.subject
661 self.log.debug("Checking if build %s has changes; commit_message "
662 "%s; repo_messages %s" % (self, commit_message,
663 repo_messages))
664 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700665 self.log.debug(" messages do not match")
666 return False
667 self.log.debug(" OK")
668 return True
669
Clark Boylanb640e052014-04-03 16:41:46 -0700670
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000671class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700672 """An Ansible launcher to be used in tests.
673
674 :ivar bool hold_jobs_in_build: If true, when jobs are launched
675 they will report that they have started but then pause until
676 released before reporting completion. This attribute may be
677 changed at any time and will take effect for subsequently
678 launched builds, but previously held builds will still need to
679 be explicitly released.
680
681 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800682 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700683 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800684 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700685 self.hold_jobs_in_build = False
686 self.lock = threading.Lock()
687 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700688 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700689 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700690 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800691
James E. Blaira5dba232016-08-08 15:53:24 -0700692 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700693 """Instruct the launcher to report matching builds as failures.
694
695 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700696 :arg Change change: The :py:class:`~tests.base.FakeChange`
697 instance which should cause the job to fail. This job
698 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700699
700 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700701 l = self.fail_tests.get(name, [])
702 l.append(change)
703 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800704
James E. Blair962220f2016-08-03 11:22:38 -0700705 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700706 """Release a held build.
707
708 :arg str regex: A regular expression which, if supplied, will
709 cause only builds with matching names to be released. If
710 not supplied, all builds will be released.
711
712 """
James E. Blair962220f2016-08-03 11:22:38 -0700713 builds = self.running_builds[:]
714 self.log.debug("Releasing build %s (%s)" % (regex,
715 len(self.running_builds)))
716 for build in builds:
717 if not regex or re.match(regex, build.name):
718 self.log.debug("Releasing build %s" %
719 (build.parameters['ZUUL_UUID']))
720 build.release()
721 else:
722 self.log.debug("Not releasing build %s" %
723 (build.parameters['ZUUL_UUID']))
724 self.log.debug("Done releasing builds %s (%s)" %
725 (regex, len(self.running_builds)))
726
James E. Blair17302972016-08-10 16:11:42 -0700727 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700728 build = FakeBuild(self, job)
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
James E. Blair17302972016-08-10 16:11:42 -0700732 super(RecordingLaunchServer, self).launchJob(job)
733
734 def stopJob(self, job):
735 self.log.debug("handle stop")
736 parameters = json.loads(job.arguments)
737 uuid = parameters['uuid']
738 for build in self.running_builds:
739 if build.unique == uuid:
740 build.aborted = True
741 build.release()
742 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700743
744 def runAnsible(self, jobdir, job):
745 build = self.job_builds[job.unique]
746 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700747
748 if self._run_ansible:
749 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
750 else:
751 result = build.run()
752
753 self.lock.acquire()
754 self.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700755 BuildHistory(name=build.name, result=result, changes=build.changes,
756 node=build.node, uuid=build.unique,
757 parameters=build.parameters,
James E. Blaire1767bc2016-08-02 10:00:27 -0700758 pipeline=build.parameters['ZUUL_PIPELINE'])
759 )
James E. Blairab7132b2016-08-05 12:36:22 -0700760 self.running_builds.remove(build)
761 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700762 self.lock.release()
Clint Byrum69e47122016-12-02 16:40:35 -0800763 if build.run_error:
764 result = None
James E. Blaire1767bc2016-08-02 10:00:27 -0700765 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800766
767
Clark Boylanb640e052014-04-03 16:41:46 -0700768class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700769 """A Gearman server for use in tests.
770
771 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
772 added to the queue but will not be distributed to workers
773 until released. This attribute may be changed at any time and
774 will take effect for subsequently enqueued jobs, but
775 previously held jobs will still need to be explicitly
776 released.
777
778 """
779
Clark Boylanb640e052014-04-03 16:41:46 -0700780 def __init__(self):
781 self.hold_jobs_in_queue = False
782 super(FakeGearmanServer, self).__init__(0)
783
784 def getJobForConnection(self, connection, peek=False):
785 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
786 for job in queue:
787 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500788 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700789 job.waiting = self.hold_jobs_in_queue
790 else:
791 job.waiting = False
792 if job.waiting:
793 continue
794 if job.name in connection.functions:
795 if not peek:
796 queue.remove(job)
797 connection.related_jobs[job.handle] = job
798 job.worker_connection = connection
799 job.running = True
800 return job
801 return None
802
803 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700804 """Release a held job.
805
806 :arg str regex: A regular expression which, if supplied, will
807 cause only jobs with matching names to be released. If
808 not supplied, all jobs will be released.
809 """
Clark Boylanb640e052014-04-03 16:41:46 -0700810 released = False
811 qlen = (len(self.high_queue) + len(self.normal_queue) +
812 len(self.low_queue))
813 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
814 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500815 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700816 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500817 parameters = json.loads(job.arguments)
818 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700819 self.log.debug("releasing queued job %s" %
820 job.unique)
821 job.waiting = False
822 released = True
823 else:
824 self.log.debug("not releasing queued job %s" %
825 job.unique)
826 if released:
827 self.wakeConnections()
828 qlen = (len(self.high_queue) + len(self.normal_queue) +
829 len(self.low_queue))
830 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
831
832
833class FakeSMTP(object):
834 log = logging.getLogger('zuul.FakeSMTP')
835
836 def __init__(self, messages, server, port):
837 self.server = server
838 self.port = port
839 self.messages = messages
840
841 def sendmail(self, from_email, to_email, msg):
842 self.log.info("Sending email from %s, to %s, with msg %s" % (
843 from_email, to_email, msg))
844
845 headers = msg.split('\n\n', 1)[0]
846 body = msg.split('\n\n', 1)[1]
847
848 self.messages.append(dict(
849 from_email=from_email,
850 to_email=to_email,
851 msg=msg,
852 headers=headers,
853 body=body,
854 ))
855
856 return True
857
858 def quit(self):
859 return True
860
861
862class FakeSwiftClientConnection(swiftclient.client.Connection):
863 def post_account(self, headers):
864 # Do nothing
865 pass
866
867 def get_auth(self):
868 # Returns endpoint and (unused) auth token
869 endpoint = os.path.join('https://storage.example.org', 'V1',
870 'AUTH_account')
871 return endpoint, ''
872
873
James E. Blair498059b2016-12-20 13:50:13 -0800874class ChrootedKazooFixture(fixtures.Fixture):
875 def __init__(self):
876 super(ChrootedKazooFixture, self).__init__()
877
878 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
879 if ':' in zk_host:
880 host, port = zk_host.split(':')
881 else:
882 host = zk_host
883 port = None
884
885 self.zookeeper_host = host
886
887 if not port:
888 self.zookeeper_port = 2181
889 else:
890 self.zookeeper_port = int(port)
891
892 def _setUp(self):
893 # Make sure the test chroot paths do not conflict
894 random_bits = ''.join(random.choice(string.ascii_lowercase +
895 string.ascii_uppercase)
896 for x in range(8))
897
898 rand_test_path = '%s_%s' % (random_bits, os.getpid())
899 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
900
901 # Ensure the chroot path exists and clean up any pre-existing znodes.
902 _tmp_client = kazoo.client.KazooClient(
903 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
904 _tmp_client.start()
905
906 if _tmp_client.exists(self.zookeeper_chroot):
907 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
908
909 _tmp_client.ensure_path(self.zookeeper_chroot)
910 _tmp_client.stop()
911 _tmp_client.close()
912
913 self.addCleanup(self._cleanup)
914
915 def _cleanup(self):
916 '''Remove the chroot path.'''
917 # Need a non-chroot'ed client to remove the chroot path
918 _tmp_client = kazoo.client.KazooClient(
919 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
920 _tmp_client.start()
921 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
922 _tmp_client.stop()
923
924
Maru Newby3fe5f852015-01-13 04:22:14 +0000925class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700926 log = logging.getLogger("zuul.test")
927
928 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000929 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700930 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
931 try:
932 test_timeout = int(test_timeout)
933 except ValueError:
934 # If timeout value is invalid do not set a timeout.
935 test_timeout = 0
936 if test_timeout > 0:
937 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
938
939 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
940 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
941 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
942 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
943 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
944 os.environ.get('OS_STDERR_CAPTURE') == '1'):
945 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
946 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
947 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
948 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair79e94b62016-10-18 08:20:22 -0700949 log_level = logging.DEBUG
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100950 if os.environ.get('OS_LOG_LEVEL') == 'DEBUG':
951 log_level = logging.DEBUG
James E. Blair79e94b62016-10-18 08:20:22 -0700952 elif os.environ.get('OS_LOG_LEVEL') == 'INFO':
953 log_level = logging.INFO
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100954 elif os.environ.get('OS_LOG_LEVEL') == 'WARNING':
955 log_level = logging.WARNING
956 elif os.environ.get('OS_LOG_LEVEL') == 'ERROR':
957 log_level = logging.ERROR
958 elif os.environ.get('OS_LOG_LEVEL') == 'CRITICAL':
959 log_level = logging.CRITICAL
Clark Boylanb640e052014-04-03 16:41:46 -0700960 self.useFixture(fixtures.FakeLogger(
Jan Hrubanb4f9c612016-01-04 18:41:04 +0100961 level=log_level,
Clark Boylanb640e052014-04-03 16:41:46 -0700962 format='%(asctime)s %(name)-32s '
963 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000964
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700965 # NOTE(notmorgan): Extract logging overrides for specific libraries
966 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
967 # each. This is used to limit the output during test runs from
968 # libraries that zuul depends on such as gear.
969 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
970
971 if log_defaults_from_env:
972 for default in log_defaults_from_env.split(','):
973 try:
974 name, level_str = default.split('=', 1)
975 level = getattr(logging, level_str, logging.DEBUG)
976 self.useFixture(fixtures.FakeLogger(
977 name=name,
978 level=level,
979 format='%(asctime)s %(name)-32s '
980 '%(levelname)-8s %(message)s'))
981 except ValueError:
982 # NOTE(notmorgan): Invalid format of the log default,
983 # skip and don't try and apply a logger for the
984 # specified module
985 pass
986
Maru Newby3fe5f852015-01-13 04:22:14 +0000987
988class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -0700989 """A test case with a functioning Zuul.
990
991 The following class variables are used during test setup and can
992 be overidden by subclasses but are effectively read-only once a
993 test method starts running:
994
995 :cvar str config_file: This points to the main zuul config file
996 within the fixtures directory. Subclasses may override this
997 to obtain a different behavior.
998
999 :cvar str tenant_config_file: This is the tenant config file
1000 (which specifies from what git repos the configuration should
1001 be loaded). It defaults to the value specified in
1002 `config_file` but can be overidden by subclasses to obtain a
1003 different tenant/project layout while using the standard main
1004 configuration.
1005
1006 The following are instance variables that are useful within test
1007 methods:
1008
1009 :ivar FakeGerritConnection fake_<connection>:
1010 A :py:class:`~tests.base.FakeGerritConnection` will be
1011 instantiated for each connection present in the config file
1012 and stored here. For instance, `fake_gerrit` will hold the
1013 FakeGerritConnection object for a connection named `gerrit`.
1014
1015 :ivar FakeGearmanServer gearman_server: An instance of
1016 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1017 server that all of the Zuul components in this test use to
1018 communicate with each other.
1019
1020 :ivar RecordingLaunchServer launch_server: An instance of
1021 :py:class:`~tests.base.RecordingLaunchServer` which is the
1022 Ansible launch server used to run jobs for this test.
1023
1024 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1025 representing currently running builds. They are appended to
1026 the list in the order they are launched, and removed from this
1027 list upon completion.
1028
1029 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1030 objects representing completed builds. They are appended to
1031 the list in the order they complete.
1032
1033 """
1034
James E. Blair83005782015-12-11 14:46:03 -08001035 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001036 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001037
1038 def _startMerger(self):
1039 self.merge_server = zuul.merger.server.MergeServer(self.config,
1040 self.connections)
1041 self.merge_server.start()
1042
Maru Newby3fe5f852015-01-13 04:22:14 +00001043 def setUp(self):
1044 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001045
1046 self.setupZK()
1047
James E. Blair97d902e2014-08-21 13:25:56 -07001048 if USE_TEMPDIR:
1049 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001050 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1051 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001052 else:
1053 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -07001054 self.test_root = os.path.join(tmp_root, "zuul-test")
1055 self.upstream_root = os.path.join(self.test_root, "upstream")
1056 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -07001057 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001058
1059 if os.path.exists(self.test_root):
1060 shutil.rmtree(self.test_root)
1061 os.makedirs(self.test_root)
1062 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001063 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001064
1065 # Make per test copy of Configuration.
1066 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001067 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001068 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001069 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -07001070 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001071 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001072
1073 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001074 # TODOv3(jeblair): remove these and replace with new git
1075 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001076 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001077 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001078 self.init_repo("org/project5")
1079 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001080 self.init_repo("org/one-job-project")
1081 self.init_repo("org/nonvoting-project")
1082 self.init_repo("org/templated-project")
1083 self.init_repo("org/layered-project")
1084 self.init_repo("org/node-project")
1085 self.init_repo("org/conflict-project")
1086 self.init_repo("org/noop-project")
1087 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001088 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001089
1090 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001091 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1092 # see: https://github.com/jsocol/pystatsd/issues/61
1093 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001094 os.environ['STATSD_PORT'] = str(self.statsd.port)
1095 self.statsd.start()
1096 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001097 reload_module(statsd)
1098 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001099
1100 self.gearman_server = FakeGearmanServer()
1101
1102 self.config.set('gearman', 'port', str(self.gearman_server.port))
1103
Joshua Hesketh352264b2015-08-11 23:42:08 +10001104 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
1105 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
1106 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001107
Joshua Hesketh352264b2015-08-11 23:42:08 +10001108 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001109
1110 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1111 FakeSwiftClientConnection))
1112 self.swift = zuul.lib.swift.Swift(self.config)
1113
Jan Hruban6b71aff2015-10-22 16:58:08 +02001114 self.event_queues = [
1115 self.sched.result_event_queue,
1116 self.sched.trigger_event_queue
1117 ]
1118
James E. Blairfef78942016-03-11 16:28:56 -08001119 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001120 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001121
Clark Boylanb640e052014-04-03 16:41:46 -07001122 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001123 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001124 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001125 return FakeURLOpener(self.upstream_root, *args, **kw)
1126
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001127 old_urlopen = urllib.request.urlopen
1128 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001129
James E. Blair3f876d52016-07-22 13:07:14 -07001130 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001131
James E. Blaire1767bc2016-08-02 10:00:27 -07001132 self.launch_server = RecordingLaunchServer(
1133 self.config, self.connections, _run_ansible=self.run_ansible)
1134 self.launch_server.start()
1135 self.history = self.launch_server.build_history
1136 self.builds = self.launch_server.running_builds
1137
1138 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001139 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001140 self.merge_client = zuul.merger.client.MergeClient(
1141 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001142 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001143
James E. Blaire1767bc2016-08-02 10:00:27 -07001144 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001145 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001146 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001147
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001148 self.webapp = zuul.webapp.WebApp(
1149 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001150 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001151
1152 self.sched.start()
1153 self.sched.reconfigure(self.config)
1154 self.sched.resume()
1155 self.webapp.start()
1156 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001157 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001158
1159 self.addCleanup(self.assertFinalState)
1160 self.addCleanup(self.shutdown)
1161
James E. Blairfef78942016-03-11 16:28:56 -08001162 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001163 # Register connections from the config
1164 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001165
Joshua Hesketh352264b2015-08-11 23:42:08 +10001166 def FakeSMTPFactory(*args, **kw):
1167 args = [self.smtp_messages] + list(args)
1168 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001169
Joshua Hesketh352264b2015-08-11 23:42:08 +10001170 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001171
Joshua Hesketh352264b2015-08-11 23:42:08 +10001172 # Set a changes database so multiple FakeGerrit's can report back to
1173 # a virtual canonical database given by the configured hostname
1174 self.gerrit_changes_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001175 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001176
Joshua Hesketh352264b2015-08-11 23:42:08 +10001177 for section_name in self.config.sections():
1178 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1179 section_name, re.I)
1180 if not con_match:
1181 continue
1182 con_name = con_match.group(2)
1183 con_config = dict(self.config.items(section_name))
1184
1185 if 'driver' not in con_config:
1186 raise Exception("No driver specified for connection %s."
1187 % con_name)
1188
1189 con_driver = con_config['driver']
1190
1191 # TODO(jhesketh): load the required class automatically
1192 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001193 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1194 self.gerrit_changes_dbs[con_config['server']] = {}
James E. Blair83005782015-12-11 14:46:03 -08001195 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001196 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001197 changes_db=self.gerrit_changes_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001198 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001199 )
James E. Blair7fc8daa2016-08-08 15:37:15 -07001200 self.event_queues.append(
1201 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001202 setattr(self, 'fake_' + con_name,
1203 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001204 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001205 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001206 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1207 else:
1208 raise Exception("Unknown driver, %s, for connection %s"
1209 % (con_config['driver'], con_name))
1210
1211 # If the [gerrit] or [smtp] sections still exist, load them in as a
1212 # connection named 'gerrit' or 'smtp' respectfully
1213
1214 if 'gerrit' in self.config.sections():
1215 self.gerrit_changes_dbs['gerrit'] = {}
James E. Blair7fc8daa2016-08-08 15:37:15 -07001216 self.event_queues.append(
1217 self.connections.connections[con_name].event_queue)
James E. Blair83005782015-12-11 14:46:03 -08001218 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001219 '_legacy_gerrit', dict(self.config.items('gerrit')),
James E. Blair7fc8daa2016-08-08 15:37:15 -07001220 changes_db=self.gerrit_changes_dbs['gerrit'])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001221
1222 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001223 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001224 zuul.connection.smtp.SMTPConnection(
1225 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001226
James E. Blair83005782015-12-11 14:46:03 -08001227 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001228 # This creates the per-test configuration object. It can be
1229 # overriden by subclasses, but should not need to be since it
1230 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001231 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001232 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001233 if hasattr(self, 'tenant_config_file'):
1234 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001235 git_path = os.path.join(
1236 os.path.dirname(
1237 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1238 'git')
1239 if os.path.exists(git_path):
1240 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001241 project = reponame.replace('_', '/')
1242 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001243 os.path.join(git_path, reponame))
1244
James E. Blair498059b2016-12-20 13:50:13 -08001245 def setupZK(self):
1246 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
1247 self.zookeeper_host = self.zk_chroot_fixture.zookeeper_host
1248 self.zookeeper_port = self.zk_chroot_fixture.zookeeper_port
1249 self.zookeeper_chroot = self.zk_chroot_fixture.zookeeper_chroot
1250
James E. Blair96c6bf82016-01-15 16:20:40 -08001251 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001252 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001253
1254 files = {}
1255 for (dirpath, dirnames, filenames) in os.walk(source_path):
1256 for filename in filenames:
1257 test_tree_filepath = os.path.join(dirpath, filename)
1258 common_path = os.path.commonprefix([test_tree_filepath,
1259 source_path])
1260 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1261 with open(test_tree_filepath, 'r') as f:
1262 content = f.read()
1263 files[relative_filepath] = content
1264 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001265 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001266
Clark Boylanb640e052014-04-03 16:41:46 -07001267 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001268 # Make sure that git.Repo objects have been garbage collected.
1269 repos = []
1270 gc.collect()
1271 for obj in gc.get_objects():
1272 if isinstance(obj, git.Repo):
1273 repos.append(obj)
1274 self.assertEqual(len(repos), 0)
1275 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001276 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001277 for tenant in self.sched.abide.tenants.values():
1278 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001279 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001280 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001281
1282 def shutdown(self):
1283 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001284 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001285 self.merge_server.stop()
1286 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001287 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001289 self.sched.stop()
1290 self.sched.join()
1291 self.statsd.stop()
1292 self.statsd.join()
1293 self.webapp.stop()
1294 self.webapp.join()
1295 self.rpc.stop()
1296 self.rpc.join()
1297 self.gearman_server.shutdown()
1298 threads = threading.enumerate()
1299 if len(threads) > 1:
1300 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001301
1302 def init_repo(self, project):
1303 parts = project.split('/')
1304 path = os.path.join(self.upstream_root, *parts[:-1])
1305 if not os.path.exists(path):
1306 os.makedirs(path)
1307 path = os.path.join(self.upstream_root, project)
1308 repo = git.Repo.init(path)
1309
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001310 with repo.config_writer() as config_writer:
1311 config_writer.set_value('user', 'email', 'user@example.com')
1312 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001313
Clark Boylanb640e052014-04-03 16:41:46 -07001314 repo.index.commit('initial commit')
1315 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001316
James E. Blair97d902e2014-08-21 13:25:56 -07001317 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001318 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001319 repo.git.clean('-x', '-f', '-d')
1320
James E. Blair97d902e2014-08-21 13:25:56 -07001321 def create_branch(self, project, branch):
1322 path = os.path.join(self.upstream_root, project)
1323 repo = git.Repo.init(path)
1324 fn = os.path.join(path, 'README')
1325
1326 branch_head = repo.create_head(branch)
1327 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001328 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001329 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001330 f.close()
1331 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001332 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001333
James E. Blair97d902e2014-08-21 13:25:56 -07001334 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001335 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001336 repo.git.clean('-x', '-f', '-d')
1337
Sachi King9f16d522016-03-16 12:20:45 +11001338 def create_commit(self, project):
1339 path = os.path.join(self.upstream_root, project)
1340 repo = git.Repo(path)
1341 repo.head.reference = repo.heads['master']
1342 file_name = os.path.join(path, 'README')
1343 with open(file_name, 'a') as f:
1344 f.write('creating fake commit\n')
1345 repo.index.add([file_name])
1346 commit = repo.index.commit('Creating a fake commit')
1347 return commit.hexsha
1348
James E. Blairb8c16472015-05-05 14:55:26 -07001349 def orderedRelease(self):
1350 # Run one build at a time to ensure non-race order:
1351 while len(self.builds):
1352 self.release(self.builds[0])
1353 self.waitUntilSettled()
1354
Clark Boylanb640e052014-04-03 16:41:46 -07001355 def release(self, job):
1356 if isinstance(job, FakeBuild):
1357 job.release()
1358 else:
1359 job.waiting = False
1360 self.log.debug("Queued job %s released" % job.unique)
1361 self.gearman_server.wakeConnections()
1362
1363 def getParameter(self, job, name):
1364 if isinstance(job, FakeBuild):
1365 return job.parameters[name]
1366 else:
1367 parameters = json.loads(job.arguments)
1368 return parameters[name]
1369
Clark Boylanb640e052014-04-03 16:41:46 -07001370 def haveAllBuildsReported(self):
1371 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001372 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001373 return False
1374 # Find out if every build that the worker has completed has been
1375 # reported back to Zuul. If it hasn't then that means a Gearman
1376 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001377 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001378 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001379 if not zbuild:
1380 # It has already been reported
1381 continue
1382 # It hasn't been reported yet.
1383 return False
1384 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001385 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001386 if connection.state == 'GRAB_WAIT':
1387 return False
1388 return True
1389
1390 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001391 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001392 for build in builds:
1393 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001394 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001395 for j in conn.related_jobs.values():
1396 if j.unique == build.uuid:
1397 client_job = j
1398 break
1399 if not client_job:
1400 self.log.debug("%s is not known to the gearman client" %
1401 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001402 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001403 if not client_job.handle:
1404 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001405 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001406 server_job = self.gearman_server.jobs.get(client_job.handle)
1407 if not server_job:
1408 self.log.debug("%s is not known to the gearman server" %
1409 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001410 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001411 if not hasattr(server_job, 'waiting'):
1412 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001413 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001414 if server_job.waiting:
1415 continue
James E. Blair17302972016-08-10 16:11:42 -07001416 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001417 self.log.debug("%s has not reported start" % build)
1418 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001419 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001420 if worker_build:
1421 if worker_build.isWaiting():
1422 continue
1423 else:
1424 self.log.debug("%s is running" % worker_build)
1425 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001426 else:
James E. Blair962220f2016-08-03 11:22:38 -07001427 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001428 return False
1429 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001430
Jan Hruban6b71aff2015-10-22 16:58:08 +02001431 def eventQueuesEmpty(self):
1432 for queue in self.event_queues:
1433 yield queue.empty()
1434
1435 def eventQueuesJoin(self):
1436 for queue in self.event_queues:
1437 queue.join()
1438
Clark Boylanb640e052014-04-03 16:41:46 -07001439 def waitUntilSettled(self):
1440 self.log.debug("Waiting until settled...")
1441 start = time.time()
1442 while True:
1443 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001444 self.log.debug("Queue status:")
1445 for queue in self.event_queues:
1446 self.log.debug(" %s: %s" % (queue, queue.empty()))
1447 self.log.debug("All builds waiting: %s" %
1448 (self.areAllBuildsWaiting(),))
James E. Blairf3156c92016-08-10 15:32:19 -07001449 self.log.debug("All builds reported: %s" %
1450 (self.haveAllBuildsReported(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001451 raise Exception("Timeout waiting for Zuul to settle")
1452 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001453
James E. Blaire1767bc2016-08-02 10:00:27 -07001454 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001455 # have all build states propogated to zuul?
1456 if self.haveAllBuildsReported():
1457 # Join ensures that the queue is empty _and_ events have been
1458 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001459 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001460 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001461 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001462 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001463 self.haveAllBuildsReported() and
1464 self.areAllBuildsWaiting()):
1465 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001466 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001467 self.log.debug("...settled.")
1468 return
1469 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001470 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001471 self.sched.wake_event.wait(0.1)
1472
1473 def countJobResults(self, jobs, result):
1474 jobs = filter(lambda x: x.result == result, jobs)
1475 return len(jobs)
1476
James E. Blair96c6bf82016-01-15 16:20:40 -08001477 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001478 for job in self.history:
1479 if (job.name == name and
1480 (project is None or
1481 job.parameters['ZUUL_PROJECT'] == project)):
1482 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001483 raise Exception("Unable to find job %s in history" % name)
1484
1485 def assertEmptyQueues(self):
1486 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001487 for tenant in self.sched.abide.tenants.values():
1488 for pipeline in tenant.layout.pipelines.values():
1489 for queue in pipeline.queues:
1490 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001491 print('pipeline %s queue %s contents %s' % (
1492 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001493 self.assertEqual(len(queue.queue), 0,
1494 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001495
1496 def assertReportedStat(self, key, value=None, kind=None):
1497 start = time.time()
1498 while time.time() < (start + 5):
1499 for stat in self.statsd.stats:
1500 pprint.pprint(self.statsd.stats)
1501 k, v = stat.split(':')
1502 if key == k:
1503 if value is None and kind is None:
1504 return
1505 elif value:
1506 if value == v:
1507 return
1508 elif kind:
1509 if v.endswith('|' + kind):
1510 return
1511 time.sleep(0.1)
1512
1513 pprint.pprint(self.statsd.stats)
1514 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001515
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001516 def assertBuilds(self, builds):
1517 """Assert that the running builds are as described.
1518
1519 The list of running builds is examined and must match exactly
1520 the list of builds described by the input.
1521
1522 :arg list builds: A list of dictionaries. Each item in the
1523 list must match the corresponding build in the build
1524 history, and each element of the dictionary must match the
1525 corresponding attribute of the build.
1526
1527 """
James E. Blair3158e282016-08-19 09:34:11 -07001528 try:
1529 self.assertEqual(len(self.builds), len(builds))
1530 for i, d in enumerate(builds):
1531 for k, v in d.items():
1532 self.assertEqual(
1533 getattr(self.builds[i], k), v,
1534 "Element %i in builds does not match" % (i,))
1535 except Exception:
1536 for build in self.builds:
1537 self.log.error("Running build: %s" % build)
1538 else:
1539 self.log.error("No running builds")
1540 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001541
James E. Blairb536ecc2016-08-31 10:11:42 -07001542 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001543 """Assert that the completed builds are as described.
1544
1545 The list of completed builds is examined and must match
1546 exactly the list of builds described by the input.
1547
1548 :arg list history: A list of dictionaries. Each item in the
1549 list must match the corresponding build in the build
1550 history, and each element of the dictionary must match the
1551 corresponding attribute of the build.
1552
James E. Blairb536ecc2016-08-31 10:11:42 -07001553 :arg bool ordered: If true, the history must match the order
1554 supplied, if false, the builds are permitted to have
1555 arrived in any order.
1556
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001557 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001558 def matches(history_item, item):
1559 for k, v in item.items():
1560 if getattr(history_item, k) != v:
1561 return False
1562 return True
James E. Blair3158e282016-08-19 09:34:11 -07001563 try:
1564 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001565 if ordered:
1566 for i, d in enumerate(history):
1567 if not matches(self.history[i], d):
1568 raise Exception(
1569 "Element %i in history does not match" % (i,))
1570 else:
1571 unseen = self.history[:]
1572 for i, d in enumerate(history):
1573 found = False
1574 for unseen_item in unseen:
1575 if matches(unseen_item, d):
1576 found = True
1577 unseen.remove(unseen_item)
1578 break
1579 if not found:
1580 raise Exception("No match found for element %i "
1581 "in history" % (i,))
1582 if unseen:
1583 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001584 except Exception:
1585 for build in self.history:
1586 self.log.error("Completed build: %s" % build)
1587 else:
1588 self.log.error("No completed builds")
1589 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001590
James E. Blair59fdbac2015-12-07 17:08:06 -08001591 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001592 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1593
1594 def updateConfigLayout(self, path):
1595 root = os.path.join(self.test_root, "config")
1596 os.makedirs(root)
1597 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1598 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001599- tenant:
1600 name: openstack
1601 source:
1602 gerrit:
1603 config-repos:
1604 - %s
1605 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001606 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001607 self.config.set('zuul', 'tenant_config',
1608 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001609
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001610 def addCommitToRepo(self, project, message, files,
1611 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001612 path = os.path.join(self.upstream_root, project)
1613 repo = git.Repo(path)
1614 repo.head.reference = branch
1615 zuul.merger.merger.reset_repo_to_head(repo)
1616 for fn, content in files.items():
1617 fn = os.path.join(path, fn)
1618 with open(fn, 'w') as f:
1619 f.write(content)
1620 repo.index.add([fn])
1621 commit = repo.index.commit(message)
1622 repo.heads[branch].commit = commit
1623 repo.head.reference = branch
1624 repo.git.clean('-x', '-f', '-d')
1625 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001626 if tag:
1627 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001628
James E. Blair7fc8daa2016-08-08 15:37:15 -07001629 def addEvent(self, connection, event):
1630 """Inject a Fake (Gerrit) event.
1631
1632 This method accepts a JSON-encoded event and simulates Zuul
1633 having received it from Gerrit. It could (and should)
1634 eventually apply to any connection type, but is currently only
1635 used with Gerrit connections. The name of the connection is
1636 used to look up the corresponding server, and the event is
1637 simulated as having been received by all Zuul connections
1638 attached to that server. So if two Gerrit connections in Zuul
1639 are connected to the same Gerrit server, and you invoke this
1640 method specifying the name of one of them, the event will be
1641 received by both.
1642
1643 .. note::
1644
1645 "self.fake_gerrit.addEvent" calls should be migrated to
1646 this method.
1647
1648 :arg str connection: The name of the connection corresponding
1649 to the gerrit server.
1650 :arg str event: The JSON-encoded event.
1651
1652 """
1653 specified_conn = self.connections.connections[connection]
1654 for conn in self.connections.connections.values():
1655 if (isinstance(conn, specified_conn.__class__) and
1656 specified_conn.server == conn.server):
1657 conn.addEvent(event)
1658
James E. Blair3f876d52016-07-22 13:07:14 -07001659
1660class AnsibleZuulTestCase(ZuulTestCase):
1661 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001662 run_ansible = True