blob: b039267ebc6c49c5dc09ce1563ec9d626456ad74 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
James E. Blair1c236df2017-02-01 14:07:24 -080031from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070032import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
38import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060039import uuid
40
Clark Boylanb640e052014-04-03 16:41:46 -070041
42import git
43import gear
44import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080045import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080046import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060047import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070048import statsd
49import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blaire511d2f2016-12-08 15:22:26 -080055import zuul.driver.gerrit.gerritsource as gerritsource
56import zuul.driver.gerrit.gerritconnection as gerritconnection
Joshua Heskethd78b4482015-09-14 16:56:34 -060057import zuul.connection.sql
Clark Boylanb640e052014-04-03 16:41:46 -070058import zuul.scheduler
59import zuul.webapp
60import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040061import zuul.executor.server
62import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080063import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070065import zuul.merger.merger
66import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070067import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080068import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070069
70FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
71 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080072
73KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070074
Clark Boylanb640e052014-04-03 16:41:46 -070075
76def repack_repo(path):
77 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
78 output = subprocess.Popen(cmd, close_fds=True,
79 stdout=subprocess.PIPE,
80 stderr=subprocess.PIPE)
81 out = output.communicate()
82 if output.returncode:
83 raise Exception("git repack returned %d" % output.returncode)
84 return out
85
86
87def random_sha1():
88 return hashlib.sha1(str(random.random())).hexdigest()
89
90
James E. Blaira190f3b2015-01-05 14:56:54 -080091def iterate_timeout(max_seconds, purpose):
92 start = time.time()
93 count = 0
94 while (time.time() < start + max_seconds):
95 count += 1
96 yield count
97 time.sleep(0)
98 raise Exception("Timeout waiting for %s" % purpose)
99
100
Clark Boylanb640e052014-04-03 16:41:46 -0700101class ChangeReference(git.Reference):
102 _common_path_default = "refs/changes"
103 _points_to_commits_only = True
104
105
106class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700107 categories = {'approved': ('Approved', -1, 1),
108 'code-review': ('Code-Review', -2, 2),
109 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700110
111 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700112 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700113 self.gerrit = gerrit
114 self.reported = 0
115 self.queried = 0
116 self.patchsets = []
117 self.number = number
118 self.project = project
119 self.branch = branch
120 self.subject = subject
121 self.latest_patchset = 0
122 self.depends_on_change = None
123 self.needed_by_changes = []
124 self.fail_merge = False
125 self.messages = []
126 self.data = {
127 'branch': branch,
128 'comments': [],
129 'commitMessage': subject,
130 'createdOn': time.time(),
131 'id': 'I' + random_sha1(),
132 'lastUpdated': time.time(),
133 'number': str(number),
134 'open': status == 'NEW',
135 'owner': {'email': 'user@example.com',
136 'name': 'User Name',
137 'username': 'username'},
138 'patchSets': self.patchsets,
139 'project': project,
140 'status': status,
141 'subject': subject,
142 'submitRecords': [],
143 'url': 'https://hostname/%s' % number}
144
145 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700146 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700147 self.data['submitRecords'] = self.getSubmitRecords()
148 self.open = status == 'NEW'
149
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700150 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700151 path = os.path.join(self.upstream_root, self.project)
152 repo = git.Repo(path)
153 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
154 self.latest_patchset),
155 'refs/tags/init')
156 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700157 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700158 repo.git.clean('-x', '-f', '-d')
159
160 path = os.path.join(self.upstream_root, self.project)
161 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700162 for fn, content in files.items():
163 fn = os.path.join(path, fn)
164 with open(fn, 'w') as f:
165 f.write(content)
166 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700167 else:
168 for fni in range(100):
169 fn = os.path.join(path, str(fni))
170 f = open(fn, 'w')
171 for ci in range(4096):
172 f.write(random.choice(string.printable))
173 f.close()
174 repo.index.add([fn])
175
176 r = repo.index.commit(msg)
177 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700178 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700179 repo.git.clean('-x', '-f', '-d')
180 repo.heads['master'].checkout()
181 return r
182
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700183 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700184 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700185 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700186 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700187 data = ("test %s %s %s\n" %
188 (self.branch, self.number, self.latest_patchset))
189 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700190 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700191 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700192 ps_files = [{'file': '/COMMIT_MSG',
193 'type': 'ADDED'},
194 {'file': 'README',
195 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700196 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700197 ps_files.append({'file': f, 'type': 'ADDED'})
198 d = {'approvals': [],
199 'createdOn': time.time(),
200 'files': ps_files,
201 'number': str(self.latest_patchset),
202 'ref': 'refs/changes/1/%s/%s' % (self.number,
203 self.latest_patchset),
204 'revision': c.hexsha,
205 'uploader': {'email': 'user@example.com',
206 'name': 'User name',
207 'username': 'user'}}
208 self.data['currentPatchSet'] = d
209 self.patchsets.append(d)
210 self.data['submitRecords'] = self.getSubmitRecords()
211
212 def getPatchsetCreatedEvent(self, patchset):
213 event = {"type": "patchset-created",
214 "change": {"project": self.project,
215 "branch": self.branch,
216 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
217 "number": str(self.number),
218 "subject": self.subject,
219 "owner": {"name": "User Name"},
220 "url": "https://hostname/3"},
221 "patchSet": self.patchsets[patchset - 1],
222 "uploader": {"name": "User Name"}}
223 return event
224
225 def getChangeRestoredEvent(self):
226 event = {"type": "change-restored",
227 "change": {"project": self.project,
228 "branch": self.branch,
229 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
230 "number": str(self.number),
231 "subject": self.subject,
232 "owner": {"name": "User Name"},
233 "url": "https://hostname/3"},
234 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100235 "patchSet": self.patchsets[-1],
236 "reason": ""}
237 return event
238
239 def getChangeAbandonedEvent(self):
240 event = {"type": "change-abandoned",
241 "change": {"project": self.project,
242 "branch": self.branch,
243 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
244 "number": str(self.number),
245 "subject": self.subject,
246 "owner": {"name": "User Name"},
247 "url": "https://hostname/3"},
248 "abandoner": {"name": "User Name"},
249 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700250 "reason": ""}
251 return event
252
253 def getChangeCommentEvent(self, patchset):
254 event = {"type": "comment-added",
255 "change": {"project": self.project,
256 "branch": self.branch,
257 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
258 "number": str(self.number),
259 "subject": self.subject,
260 "owner": {"name": "User Name"},
261 "url": "https://hostname/3"},
262 "patchSet": self.patchsets[patchset - 1],
263 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700264 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700265 "description": "Code-Review",
266 "value": "0"}],
267 "comment": "This is a comment"}
268 return event
269
James E. Blairc2a5ed72017-02-20 14:12:01 -0500270 def getChangeMergedEvent(self):
271 event = {"submitter": {"name": "Jenkins",
272 "username": "jenkins"},
273 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
274 "patchSet": self.patchsets[-1],
275 "change": self.data,
276 "type": "change-merged",
277 "eventCreatedOn": 1487613810}
278 return event
279
James E. Blair8cce42e2016-10-18 08:18:36 -0700280 def getRefUpdatedEvent(self):
281 path = os.path.join(self.upstream_root, self.project)
282 repo = git.Repo(path)
283 oldrev = repo.heads[self.branch].commit.hexsha
284
285 event = {
286 "type": "ref-updated",
287 "submitter": {
288 "name": "User Name",
289 },
290 "refUpdate": {
291 "oldRev": oldrev,
292 "newRev": self.patchsets[-1]['revision'],
293 "refName": self.branch,
294 "project": self.project,
295 }
296 }
297 return event
298
Joshua Hesketh642824b2014-07-01 17:54:59 +1000299 def addApproval(self, category, value, username='reviewer_john',
300 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700301 if not granted_on:
302 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000303 approval = {
304 'description': self.categories[category][0],
305 'type': category,
306 'value': str(value),
307 'by': {
308 'username': username,
309 'email': username + '@example.com',
310 },
311 'grantedOn': int(granted_on)
312 }
Clark Boylanb640e052014-04-03 16:41:46 -0700313 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
314 if x['by']['username'] == username and x['type'] == category:
315 del self.patchsets[-1]['approvals'][i]
316 self.patchsets[-1]['approvals'].append(approval)
317 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000318 'author': {'email': 'author@example.com',
319 'name': 'Patchset Author',
320 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700321 'change': {'branch': self.branch,
322 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
323 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000324 'owner': {'email': 'owner@example.com',
325 'name': 'Change Owner',
326 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700327 'project': self.project,
328 'subject': self.subject,
329 'topic': 'master',
330 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000331 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700332 'patchSet': self.patchsets[-1],
333 'type': 'comment-added'}
334 self.data['submitRecords'] = self.getSubmitRecords()
335 return json.loads(json.dumps(event))
336
337 def getSubmitRecords(self):
338 status = {}
339 for cat in self.categories.keys():
340 status[cat] = 0
341
342 for a in self.patchsets[-1]['approvals']:
343 cur = status[a['type']]
344 cat_min, cat_max = self.categories[a['type']][1:]
345 new = int(a['value'])
346 if new == cat_min:
347 cur = new
348 elif abs(new) > abs(cur):
349 cur = new
350 status[a['type']] = cur
351
352 labels = []
353 ok = True
354 for typ, cat in self.categories.items():
355 cur = status[typ]
356 cat_min, cat_max = cat[1:]
357 if cur == cat_min:
358 value = 'REJECT'
359 ok = False
360 elif cur == cat_max:
361 value = 'OK'
362 else:
363 value = 'NEED'
364 ok = False
365 labels.append({'label': cat[0], 'status': value})
366 if ok:
367 return [{'status': 'OK'}]
368 return [{'status': 'NOT_READY',
369 'labels': labels}]
370
371 def setDependsOn(self, other, patchset):
372 self.depends_on_change = other
373 d = {'id': other.data['id'],
374 'number': other.data['number'],
375 'ref': other.patchsets[patchset - 1]['ref']
376 }
377 self.data['dependsOn'] = [d]
378
379 other.needed_by_changes.append(self)
380 needed = other.data.get('neededBy', [])
381 d = {'id': self.data['id'],
382 'number': self.data['number'],
383 'ref': self.patchsets[patchset - 1]['ref'],
384 'revision': self.patchsets[patchset - 1]['revision']
385 }
386 needed.append(d)
387 other.data['neededBy'] = needed
388
389 def query(self):
390 self.queried += 1
391 d = self.data.get('dependsOn')
392 if d:
393 d = d[0]
394 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
395 d['isCurrentPatchSet'] = True
396 else:
397 d['isCurrentPatchSet'] = False
398 return json.loads(json.dumps(self.data))
399
400 def setMerged(self):
401 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000402 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700403 return
404 if self.fail_merge:
405 return
406 self.data['status'] = 'MERGED'
407 self.open = False
408
409 path = os.path.join(self.upstream_root, self.project)
410 repo = git.Repo(path)
411 repo.heads[self.branch].commit = \
412 repo.commit(self.patchsets[-1]['revision'])
413
414 def setReported(self):
415 self.reported += 1
416
417
James E. Blaire511d2f2016-12-08 15:22:26 -0800418class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700419 """A Fake Gerrit connection for use in tests.
420
421 This subclasses
422 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
423 ability for tests to add changes to the fake Gerrit it represents.
424 """
425
Joshua Hesketh352264b2015-08-11 23:42:08 +1000426 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700427
James E. Blaire511d2f2016-12-08 15:22:26 -0800428 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700429 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800430 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000431 connection_config)
432
James E. Blair7fc8daa2016-08-08 15:37:15 -0700433 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700434 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
435 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000436 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700437 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200438 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700439
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700440 def addFakeChange(self, project, branch, subject, status='NEW',
441 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700442 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700443 self.change_number += 1
444 c = FakeChange(self, self.change_number, project, branch, subject,
445 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700446 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700447 self.changes[self.change_number] = c
448 return c
449
Clark Boylanb640e052014-04-03 16:41:46 -0700450 def review(self, project, changeid, message, action):
451 number, ps = changeid.split(',')
452 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000453
454 # Add the approval back onto the change (ie simulate what gerrit would
455 # do).
456 # Usually when zuul leaves a review it'll create a feedback loop where
457 # zuul's review enters another gerrit event (which is then picked up by
458 # zuul). However, we can't mimic this behaviour (by adding this
459 # approval event into the queue) as it stops jobs from checking what
460 # happens before this event is triggered. If a job needs to see what
461 # happens they can add their own verified event into the queue.
462 # Nevertheless, we can update change with the new review in gerrit.
463
James E. Blair8b5408c2016-08-08 15:37:46 -0700464 for cat in action.keys():
465 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000467
James E. Blair8b5408c2016-08-08 15:37:46 -0700468 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000469 if 'label' in action:
470 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000471 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000472
Clark Boylanb640e052014-04-03 16:41:46 -0700473 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000474
Clark Boylanb640e052014-04-03 16:41:46 -0700475 if 'submit' in action:
476 change.setMerged()
477 if message:
478 change.setReported()
479
480 def query(self, number):
481 change = self.changes.get(int(number))
482 if change:
483 return change.query()
484 return {}
485
James E. Blairc494d542014-08-06 09:23:52 -0700486 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700487 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700488 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800489 if query.startswith('change:'):
490 # Query a specific changeid
491 changeid = query[len('change:'):]
492 l = [change.query() for change in self.changes.values()
493 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700494 elif query.startswith('message:'):
495 # Query the content of a commit message
496 msg = query[len('message:'):].strip()
497 l = [change.query() for change in self.changes.values()
498 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800499 else:
500 # Query all open changes
501 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700502 return l
James E. Blairc494d542014-08-06 09:23:52 -0700503
Joshua Hesketh352264b2015-08-11 23:42:08 +1000504 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700505 pass
506
Joshua Hesketh352264b2015-08-11 23:42:08 +1000507 def getGitUrl(self, project):
508 return os.path.join(self.upstream_root, project.name)
509
Clark Boylanb640e052014-04-03 16:41:46 -0700510
511class BuildHistory(object):
512 def __init__(self, **kw):
513 self.__dict__.update(kw)
514
515 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700516 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
517 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700518
519
520class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200521 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700522 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700523 self.url = url
524
525 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700526 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700527 path = res.path
528 project = '/'.join(path.split('/')[2:-2])
529 ret = '001e# service=git-upload-pack\n'
530 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
531 'multi_ack thin-pack side-band side-band-64k ofs-delta '
532 'shallow no-progress include-tag multi_ack_detailed no-done\n')
533 path = os.path.join(self.upstream_root, project)
534 repo = git.Repo(path)
535 for ref in repo.refs:
536 r = ref.object.hexsha + ' ' + ref.path + '\n'
537 ret += '%04x%s' % (len(r) + 4, r)
538 ret += '0000'
539 return ret
540
541
Clark Boylanb640e052014-04-03 16:41:46 -0700542class FakeStatsd(threading.Thread):
543 def __init__(self):
544 threading.Thread.__init__(self)
545 self.daemon = True
546 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
547 self.sock.bind(('', 0))
548 self.port = self.sock.getsockname()[1]
549 self.wake_read, self.wake_write = os.pipe()
550 self.stats = []
551
552 def run(self):
553 while True:
554 poll = select.poll()
555 poll.register(self.sock, select.POLLIN)
556 poll.register(self.wake_read, select.POLLIN)
557 ret = poll.poll()
558 for (fd, event) in ret:
559 if fd == self.sock.fileno():
560 data = self.sock.recvfrom(1024)
561 if not data:
562 return
563 self.stats.append(data[0])
564 if fd == self.wake_read:
565 return
566
567 def stop(self):
568 os.write(self.wake_write, '1\n')
569
570
James E. Blaire1767bc2016-08-02 10:00:27 -0700571class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700572 log = logging.getLogger("zuul.test")
573
Paul Belanger174a8272017-03-14 13:20:10 -0400574 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700575 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400576 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700577 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700578 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700579 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700580 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700581 # TODOv3(jeblair): self.node is really "the image of the node
582 # assigned". We should rename it (self.node_image?) if we
583 # keep using it like this, or we may end up exposing more of
584 # the complexity around multi-node jobs here
585 # (self.nodes[0].image?)
586 self.node = None
587 if len(self.parameters.get('nodes')) == 1:
588 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700589 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100590 self.pipeline = self.parameters['ZUUL_PIPELINE']
591 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700592 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700593 self.wait_condition = threading.Condition()
594 self.waiting = False
595 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500596 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700597 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700598 self.changes = None
599 if 'ZUUL_CHANGE_IDS' in self.parameters:
600 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700601
James E. Blair3158e282016-08-19 09:34:11 -0700602 def __repr__(self):
603 waiting = ''
604 if self.waiting:
605 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100606 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
607 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700608
Clark Boylanb640e052014-04-03 16:41:46 -0700609 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700610 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700611 self.wait_condition.acquire()
612 self.wait_condition.notify()
613 self.waiting = False
614 self.log.debug("Build %s released" % self.unique)
615 self.wait_condition.release()
616
617 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700618 """Return whether this build is being held.
619
620 :returns: Whether the build is being held.
621 :rtype: bool
622 """
623
Clark Boylanb640e052014-04-03 16:41:46 -0700624 self.wait_condition.acquire()
625 if self.waiting:
626 ret = True
627 else:
628 ret = False
629 self.wait_condition.release()
630 return ret
631
632 def _wait(self):
633 self.wait_condition.acquire()
634 self.waiting = True
635 self.log.debug("Build %s waiting" % self.unique)
636 self.wait_condition.wait()
637 self.wait_condition.release()
638
639 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700640 self.log.debug('Running build %s' % self.unique)
641
Paul Belanger174a8272017-03-14 13:20:10 -0400642 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700643 self.log.debug('Holding build %s' % self.unique)
644 self._wait()
645 self.log.debug("Build %s continuing" % self.unique)
646
James E. Blair412fba82017-01-26 15:00:50 -0800647 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700648 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800649 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700650 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800651 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500652 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800653 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700654
James E. Blaire1767bc2016-08-02 10:00:27 -0700655 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700656
James E. Blaira5dba232016-08-08 15:53:24 -0700657 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400658 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700659 for change in changes:
660 if self.hasChanges(change):
661 return True
662 return False
663
James E. Blaire7b99a02016-08-05 14:27:34 -0700664 def hasChanges(self, *changes):
665 """Return whether this build has certain changes in its git repos.
666
667 :arg FakeChange changes: One or more changes (varargs) that
668 are expected to be present (in order) in the git repository of
669 the active project.
670
671 :returns: Whether the build has the indicated changes.
672 :rtype: bool
673
674 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800675 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500676 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800677 try:
678 repo = git.Repo(path)
679 except NoSuchPathError as e:
680 self.log.debug('%s' % e)
681 return False
682 ref = self.parameters['ZUUL_REF']
683 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
684 commit_message = '%s-1' % change.subject
685 self.log.debug("Checking if build %s has changes; commit_message "
686 "%s; repo_messages %s" % (self, commit_message,
687 repo_messages))
688 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700689 self.log.debug(" messages do not match")
690 return False
691 self.log.debug(" OK")
692 return True
693
Clark Boylanb640e052014-04-03 16:41:46 -0700694
Paul Belanger174a8272017-03-14 13:20:10 -0400695class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
696 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700697
Paul Belanger174a8272017-03-14 13:20:10 -0400698 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700699 they will report that they have started but then pause until
700 released before reporting completion. This attribute may be
701 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400702 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700703 be explicitly released.
704
705 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800706 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700707 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800708 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400709 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700710 self.hold_jobs_in_build = False
711 self.lock = threading.Lock()
712 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700713 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700714 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700715 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800716
James E. Blaira5dba232016-08-08 15:53:24 -0700717 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400718 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700719
720 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700721 :arg Change change: The :py:class:`~tests.base.FakeChange`
722 instance which should cause the job to fail. This job
723 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700724
725 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700726 l = self.fail_tests.get(name, [])
727 l.append(change)
728 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800729
James E. Blair962220f2016-08-03 11:22:38 -0700730 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700731 """Release a held build.
732
733 :arg str regex: A regular expression which, if supplied, will
734 cause only builds with matching names to be released. If
735 not supplied, all builds will be released.
736
737 """
James E. Blair962220f2016-08-03 11:22:38 -0700738 builds = self.running_builds[:]
739 self.log.debug("Releasing build %s (%s)" % (regex,
740 len(self.running_builds)))
741 for build in builds:
742 if not regex or re.match(regex, build.name):
743 self.log.debug("Releasing build %s" %
744 (build.parameters['ZUUL_UUID']))
745 build.release()
746 else:
747 self.log.debug("Not releasing build %s" %
748 (build.parameters['ZUUL_UUID']))
749 self.log.debug("Done releasing builds %s (%s)" %
750 (regex, len(self.running_builds)))
751
Paul Belanger174a8272017-03-14 13:20:10 -0400752 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700753 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700754 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700755 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700756 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800757 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500758 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800759 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100760 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
761 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700762
763 def stopJob(self, job):
764 self.log.debug("handle stop")
765 parameters = json.loads(job.arguments)
766 uuid = parameters['uuid']
767 for build in self.running_builds:
768 if build.unique == uuid:
769 build.aborted = True
770 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400771 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700772
Joshua Hesketh50c21782016-10-13 21:34:14 +1100773
Paul Belanger174a8272017-03-14 13:20:10 -0400774class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
Paul Belanger96618ed2017-03-01 09:42:33 -0500775 def runPlaybooks(self, args):
Paul Belanger174a8272017-03-14 13:20:10 -0400776 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800777 build.jobdir = self.jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700778
Paul Belanger96618ed2017-03-01 09:42:33 -0500779 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
James E. Blair412fba82017-01-26 15:00:50 -0800780
Paul Belanger174a8272017-03-14 13:20:10 -0400781 self.executor_server.lock.acquire()
782 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700783 BuildHistory(name=build.name, result=result, changes=build.changes,
784 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800785 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700786 pipeline=build.parameters['ZUUL_PIPELINE'])
787 )
Paul Belanger174a8272017-03-14 13:20:10 -0400788 self.executor_server.running_builds.remove(build)
789 del self.executor_server.job_builds[self.job.unique]
790 self.executor_server.lock.release()
James E. Blair412fba82017-01-26 15:00:50 -0800791 return result
792
Monty Taylore6562aa2017-02-20 07:37:39 -0500793 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400794 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800795
Paul Belanger174a8272017-03-14 13:20:10 -0400796 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600797 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500798 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800799 else:
800 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700801 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800802
James E. Blairad8dca02017-02-21 11:48:32 -0500803 def getHostList(self, args):
804 self.log.debug("hostlist")
805 hosts = super(RecordingAnsibleJob, self).getHostList(args)
806 for name, d in hosts:
807 d['ansible_connection'] = 'local'
808 hosts.append(('localhost', dict(ansible_connection='local')))
809 return hosts
810
James E. Blairf5dbd002015-12-23 15:26:17 -0800811
Clark Boylanb640e052014-04-03 16:41:46 -0700812class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700813 """A Gearman server for use in tests.
814
815 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
816 added to the queue but will not be distributed to workers
817 until released. This attribute may be changed at any time and
818 will take effect for subsequently enqueued jobs, but
819 previously held jobs will still need to be explicitly
820 released.
821
822 """
823
Clark Boylanb640e052014-04-03 16:41:46 -0700824 def __init__(self):
825 self.hold_jobs_in_queue = False
826 super(FakeGearmanServer, self).__init__(0)
827
828 def getJobForConnection(self, connection, peek=False):
829 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
830 for job in queue:
831 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400832 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700833 job.waiting = self.hold_jobs_in_queue
834 else:
835 job.waiting = False
836 if job.waiting:
837 continue
838 if job.name in connection.functions:
839 if not peek:
840 queue.remove(job)
841 connection.related_jobs[job.handle] = job
842 job.worker_connection = connection
843 job.running = True
844 return job
845 return None
846
847 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700848 """Release a held job.
849
850 :arg str regex: A regular expression which, if supplied, will
851 cause only jobs with matching names to be released. If
852 not supplied, all jobs will be released.
853 """
Clark Boylanb640e052014-04-03 16:41:46 -0700854 released = False
855 qlen = (len(self.high_queue) + len(self.normal_queue) +
856 len(self.low_queue))
857 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
858 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400859 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700860 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500861 parameters = json.loads(job.arguments)
862 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700863 self.log.debug("releasing queued job %s" %
864 job.unique)
865 job.waiting = False
866 released = True
867 else:
868 self.log.debug("not releasing queued job %s" %
869 job.unique)
870 if released:
871 self.wakeConnections()
872 qlen = (len(self.high_queue) + len(self.normal_queue) +
873 len(self.low_queue))
874 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
875
876
877class FakeSMTP(object):
878 log = logging.getLogger('zuul.FakeSMTP')
879
880 def __init__(self, messages, server, port):
881 self.server = server
882 self.port = port
883 self.messages = messages
884
885 def sendmail(self, from_email, to_email, msg):
886 self.log.info("Sending email from %s, to %s, with msg %s" % (
887 from_email, to_email, msg))
888
889 headers = msg.split('\n\n', 1)[0]
890 body = msg.split('\n\n', 1)[1]
891
892 self.messages.append(dict(
893 from_email=from_email,
894 to_email=to_email,
895 msg=msg,
896 headers=headers,
897 body=body,
898 ))
899
900 return True
901
902 def quit(self):
903 return True
904
905
James E. Blairdce6cea2016-12-20 16:45:32 -0800906class FakeNodepool(object):
907 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800908 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800909
910 log = logging.getLogger("zuul.test.FakeNodepool")
911
912 def __init__(self, host, port, chroot):
913 self.client = kazoo.client.KazooClient(
914 hosts='%s:%s%s' % (host, port, chroot))
915 self.client.start()
916 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800917 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800918 self.thread = threading.Thread(target=self.run)
919 self.thread.daemon = True
920 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800921 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800922
923 def stop(self):
924 self._running = False
925 self.thread.join()
926 self.client.stop()
927 self.client.close()
928
929 def run(self):
930 while self._running:
931 self._run()
932 time.sleep(0.1)
933
934 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800935 if self.paused:
936 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800937 for req in self.getNodeRequests():
938 self.fulfillRequest(req)
939
940 def getNodeRequests(self):
941 try:
942 reqids = self.client.get_children(self.REQUEST_ROOT)
943 except kazoo.exceptions.NoNodeError:
944 return []
945 reqs = []
946 for oid in sorted(reqids):
947 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800948 try:
949 data, stat = self.client.get(path)
950 data = json.loads(data)
951 data['_oid'] = oid
952 reqs.append(data)
953 except kazoo.exceptions.NoNodeError:
954 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800955 return reqs
956
James E. Blaire18d4602017-01-05 11:17:28 -0800957 def getNodes(self):
958 try:
959 nodeids = self.client.get_children(self.NODE_ROOT)
960 except kazoo.exceptions.NoNodeError:
961 return []
962 nodes = []
963 for oid in sorted(nodeids):
964 path = self.NODE_ROOT + '/' + oid
965 data, stat = self.client.get(path)
966 data = json.loads(data)
967 data['_oid'] = oid
968 try:
969 lockfiles = self.client.get_children(path + '/lock')
970 except kazoo.exceptions.NoNodeError:
971 lockfiles = []
972 if lockfiles:
973 data['_lock'] = True
974 else:
975 data['_lock'] = False
976 nodes.append(data)
977 return nodes
978
James E. Blaira38c28e2017-01-04 10:33:20 -0800979 def makeNode(self, request_id, node_type):
980 now = time.time()
981 path = '/nodepool/nodes/'
982 data = dict(type=node_type,
983 provider='test-provider',
984 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -0400985 az='test-az',
James E. Blaira38c28e2017-01-04 10:33:20 -0800986 public_ipv4='127.0.0.1',
987 private_ipv4=None,
988 public_ipv6=None,
989 allocated_to=request_id,
990 state='ready',
991 state_time=now,
992 created_time=now,
993 updated_time=now,
994 image_id=None,
Paul Belanger174a8272017-03-14 13:20:10 -0400995 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -0800996 data = json.dumps(data)
997 path = self.client.create(path, data,
998 makepath=True,
999 sequence=True)
1000 nodeid = path.split("/")[-1]
1001 return nodeid
1002
James E. Blair6ab79e02017-01-06 10:10:17 -08001003 def addFailRequest(self, request):
1004 self.fail_requests.add(request['_oid'])
1005
James E. Blairdce6cea2016-12-20 16:45:32 -08001006 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001007 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001008 return
1009 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001010 oid = request['_oid']
1011 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001012
James E. Blair6ab79e02017-01-06 10:10:17 -08001013 if oid in self.fail_requests:
1014 request['state'] = 'failed'
1015 else:
1016 request['state'] = 'fulfilled'
1017 nodes = []
1018 for node in request['node_types']:
1019 nodeid = self.makeNode(oid, node)
1020 nodes.append(nodeid)
1021 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001022
James E. Blaira38c28e2017-01-04 10:33:20 -08001023 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001024 path = self.REQUEST_ROOT + '/' + oid
1025 data = json.dumps(request)
1026 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1027 self.client.set(path, data)
1028
1029
James E. Blair498059b2016-12-20 13:50:13 -08001030class ChrootedKazooFixture(fixtures.Fixture):
1031 def __init__(self):
1032 super(ChrootedKazooFixture, self).__init__()
1033
1034 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1035 if ':' in zk_host:
1036 host, port = zk_host.split(':')
1037 else:
1038 host = zk_host
1039 port = None
1040
1041 self.zookeeper_host = host
1042
1043 if not port:
1044 self.zookeeper_port = 2181
1045 else:
1046 self.zookeeper_port = int(port)
1047
1048 def _setUp(self):
1049 # Make sure the test chroot paths do not conflict
1050 random_bits = ''.join(random.choice(string.ascii_lowercase +
1051 string.ascii_uppercase)
1052 for x in range(8))
1053
1054 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1055 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1056
1057 # Ensure the chroot path exists and clean up any pre-existing znodes.
1058 _tmp_client = kazoo.client.KazooClient(
1059 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1060 _tmp_client.start()
1061
1062 if _tmp_client.exists(self.zookeeper_chroot):
1063 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1064
1065 _tmp_client.ensure_path(self.zookeeper_chroot)
1066 _tmp_client.stop()
1067 _tmp_client.close()
1068
1069 self.addCleanup(self._cleanup)
1070
1071 def _cleanup(self):
1072 '''Remove the chroot path.'''
1073 # Need a non-chroot'ed client to remove the chroot path
1074 _tmp_client = kazoo.client.KazooClient(
1075 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1076 _tmp_client.start()
1077 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1078 _tmp_client.stop()
1079
1080
Joshua Heskethd78b4482015-09-14 16:56:34 -06001081class MySQLSchemaFixture(fixtures.Fixture):
1082 def setUp(self):
1083 super(MySQLSchemaFixture, self).setUp()
1084
1085 random_bits = ''.join(random.choice(string.ascii_lowercase +
1086 string.ascii_uppercase)
1087 for x in range(8))
1088 self.name = '%s_%s' % (random_bits, os.getpid())
1089 self.passwd = uuid.uuid4().hex
1090 db = pymysql.connect(host="localhost",
1091 user="openstack_citest",
1092 passwd="openstack_citest",
1093 db="openstack_citest")
1094 cur = db.cursor()
1095 cur.execute("create database %s" % self.name)
1096 cur.execute(
1097 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1098 (self.name, self.name, self.passwd))
1099 cur.execute("flush privileges")
1100
1101 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1102 self.passwd,
1103 self.name)
1104 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1105 self.addCleanup(self.cleanup)
1106
1107 def cleanup(self):
1108 db = pymysql.connect(host="localhost",
1109 user="openstack_citest",
1110 passwd="openstack_citest",
1111 db="openstack_citest")
1112 cur = db.cursor()
1113 cur.execute("drop database %s" % self.name)
1114 cur.execute("drop user '%s'@'localhost'" % self.name)
1115 cur.execute("flush privileges")
1116
1117
Maru Newby3fe5f852015-01-13 04:22:14 +00001118class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001119 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001120 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001121
James E. Blair1c236df2017-02-01 14:07:24 -08001122 def attachLogs(self, *args):
1123 def reader():
1124 self._log_stream.seek(0)
1125 while True:
1126 x = self._log_stream.read(4096)
1127 if not x:
1128 break
1129 yield x.encode('utf8')
1130 content = testtools.content.content_from_reader(
1131 reader,
1132 testtools.content_type.UTF8_TEXT,
1133 False)
1134 self.addDetail('logging', content)
1135
Clark Boylanb640e052014-04-03 16:41:46 -07001136 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001137 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001138 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1139 try:
1140 test_timeout = int(test_timeout)
1141 except ValueError:
1142 # If timeout value is invalid do not set a timeout.
1143 test_timeout = 0
1144 if test_timeout > 0:
1145 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1146
1147 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1148 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1149 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1150 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1151 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1152 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1153 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1154 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1155 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1156 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001157 self._log_stream = StringIO()
1158 self.addOnException(self.attachLogs)
1159 else:
1160 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001161
James E. Blair1c236df2017-02-01 14:07:24 -08001162 handler = logging.StreamHandler(self._log_stream)
1163 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1164 '%(levelname)-8s %(message)s')
1165 handler.setFormatter(formatter)
1166
1167 logger = logging.getLogger()
1168 logger.setLevel(logging.DEBUG)
1169 logger.addHandler(handler)
1170
1171 # NOTE(notmorgan): Extract logging overrides for specific
1172 # libraries from the OS_LOG_DEFAULTS env and create loggers
1173 # for each. This is used to limit the output during test runs
1174 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001175 log_defaults_from_env = os.environ.get(
1176 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001177 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001178
James E. Blairdce6cea2016-12-20 16:45:32 -08001179 if log_defaults_from_env:
1180 for default in log_defaults_from_env.split(','):
1181 try:
1182 name, level_str = default.split('=', 1)
1183 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001184 logger = logging.getLogger(name)
1185 logger.setLevel(level)
1186 logger.addHandler(handler)
1187 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001188 except ValueError:
1189 # NOTE(notmorgan): Invalid format of the log default,
1190 # skip and don't try and apply a logger for the
1191 # specified module
1192 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001193
Maru Newby3fe5f852015-01-13 04:22:14 +00001194
1195class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001196 """A test case with a functioning Zuul.
1197
1198 The following class variables are used during test setup and can
1199 be overidden by subclasses but are effectively read-only once a
1200 test method starts running:
1201
1202 :cvar str config_file: This points to the main zuul config file
1203 within the fixtures directory. Subclasses may override this
1204 to obtain a different behavior.
1205
1206 :cvar str tenant_config_file: This is the tenant config file
1207 (which specifies from what git repos the configuration should
1208 be loaded). It defaults to the value specified in
1209 `config_file` but can be overidden by subclasses to obtain a
1210 different tenant/project layout while using the standard main
1211 configuration.
1212
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001213 :cvar bool create_project_keys: Indicates whether Zuul should
1214 auto-generate keys for each project, or whether the test
1215 infrastructure should insert dummy keys to save time during
1216 startup. Defaults to False.
1217
James E. Blaire7b99a02016-08-05 14:27:34 -07001218 The following are instance variables that are useful within test
1219 methods:
1220
1221 :ivar FakeGerritConnection fake_<connection>:
1222 A :py:class:`~tests.base.FakeGerritConnection` will be
1223 instantiated for each connection present in the config file
1224 and stored here. For instance, `fake_gerrit` will hold the
1225 FakeGerritConnection object for a connection named `gerrit`.
1226
1227 :ivar FakeGearmanServer gearman_server: An instance of
1228 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1229 server that all of the Zuul components in this test use to
1230 communicate with each other.
1231
Paul Belanger174a8272017-03-14 13:20:10 -04001232 :ivar RecordingExecutorServer executor_server: An instance of
1233 :py:class:`~tests.base.RecordingExecutorServer` which is the
1234 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001235
1236 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1237 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001238 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001239 list upon completion.
1240
1241 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1242 objects representing completed builds. They are appended to
1243 the list in the order they complete.
1244
1245 """
1246
James E. Blair83005782015-12-11 14:46:03 -08001247 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001248 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001249 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001250
1251 def _startMerger(self):
1252 self.merge_server = zuul.merger.server.MergeServer(self.config,
1253 self.connections)
1254 self.merge_server.start()
1255
Maru Newby3fe5f852015-01-13 04:22:14 +00001256 def setUp(self):
1257 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001258
1259 self.setupZK()
1260
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001261 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001262 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001263 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1264 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001265 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001266 tmp_root = tempfile.mkdtemp(
1267 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001268 self.test_root = os.path.join(tmp_root, "zuul-test")
1269 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001270 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001271 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001272 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001273
1274 if os.path.exists(self.test_root):
1275 shutil.rmtree(self.test_root)
1276 os.makedirs(self.test_root)
1277 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001278 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001279
1280 # Make per test copy of Configuration.
1281 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001282 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001283 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001284 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001285 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001286 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001287 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001288
1289 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001290 # TODOv3(jeblair): remove these and replace with new git
1291 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001292 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001293 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001294 self.init_repo("org/project5")
1295 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001296 self.init_repo("org/one-job-project")
1297 self.init_repo("org/nonvoting-project")
1298 self.init_repo("org/templated-project")
1299 self.init_repo("org/layered-project")
1300 self.init_repo("org/node-project")
1301 self.init_repo("org/conflict-project")
1302 self.init_repo("org/noop-project")
1303 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001304 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001305
1306 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001307 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1308 # see: https://github.com/jsocol/pystatsd/issues/61
1309 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001310 os.environ['STATSD_PORT'] = str(self.statsd.port)
1311 self.statsd.start()
1312 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001313 reload_module(statsd)
1314 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001315
1316 self.gearman_server = FakeGearmanServer()
1317
1318 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001319 self.log.info("Gearman server on port %s" %
1320 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001321
James E. Blaire511d2f2016-12-08 15:22:26 -08001322 gerritsource.GerritSource.replication_timeout = 1.5
1323 gerritsource.GerritSource.replication_retry_interval = 0.5
1324 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001325
Joshua Hesketh352264b2015-08-11 23:42:08 +10001326 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001327
Jan Hruban6b71aff2015-10-22 16:58:08 +02001328 self.event_queues = [
1329 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001330 self.sched.trigger_event_queue,
1331 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001332 ]
1333
James E. Blairfef78942016-03-11 16:28:56 -08001334 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001335 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001336
Clark Boylanb640e052014-04-03 16:41:46 -07001337 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001338 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001339 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001340 return FakeURLOpener(self.upstream_root, *args, **kw)
1341
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001342 old_urlopen = urllib.request.urlopen
1343 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001344
James E. Blair3f876d52016-07-22 13:07:14 -07001345 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001346
Paul Belanger174a8272017-03-14 13:20:10 -04001347 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001348 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001349 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001350 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001351 _test_root=self.test_root,
1352 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001353 self.executor_server.start()
1354 self.history = self.executor_server.build_history
1355 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001356
Paul Belanger174a8272017-03-14 13:20:10 -04001357 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001358 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001359 self.merge_client = zuul.merger.client.MergeClient(
1360 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001361 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001362 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001363 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001364
James E. Blair0d5a36e2017-02-21 10:53:44 -05001365 self.fake_nodepool = FakeNodepool(
1366 self.zk_chroot_fixture.zookeeper_host,
1367 self.zk_chroot_fixture.zookeeper_port,
1368 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001369
Paul Belanger174a8272017-03-14 13:20:10 -04001370 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001371 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001372 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001373 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001374
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001375 self.webapp = zuul.webapp.WebApp(
1376 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001377 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001378
1379 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001380 self.webapp.start()
1381 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001382 self.executor_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001383 self.addCleanup(self.shutdown)
1384
James E. Blairb9c0d772017-03-03 14:34:49 -08001385 self.sched.reconfigure(self.config)
1386 self.sched.resume()
1387
James E. Blaire18d4602017-01-05 11:17:28 -08001388 def tearDown(self):
1389 super(ZuulTestCase, self).tearDown()
1390 self.assertFinalState()
1391
James E. Blairfef78942016-03-11 16:28:56 -08001392 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001393 # Set up gerrit related fakes
1394 # Set a changes database so multiple FakeGerrit's can report back to
1395 # a virtual canonical database given by the configured hostname
1396 self.gerrit_changes_dbs = {}
1397
1398 def getGerritConnection(driver, name, config):
1399 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1400 con = FakeGerritConnection(driver, name, config,
1401 changes_db=db,
1402 upstream_root=self.upstream_root)
1403 self.event_queues.append(con.event_queue)
1404 setattr(self, 'fake_' + name, con)
1405 return con
1406
1407 self.useFixture(fixtures.MonkeyPatch(
1408 'zuul.driver.gerrit.GerritDriver.getConnection',
1409 getGerritConnection))
1410
1411 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001412 # TODO(jhesketh): This should come from lib.connections for better
1413 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001414 # Register connections from the config
1415 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001416
Joshua Hesketh352264b2015-08-11 23:42:08 +10001417 def FakeSMTPFactory(*args, **kw):
1418 args = [self.smtp_messages] + list(args)
1419 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001420
Joshua Hesketh352264b2015-08-11 23:42:08 +10001421 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001422
James E. Blaire511d2f2016-12-08 15:22:26 -08001423 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001424 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001425 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001426
James E. Blair83005782015-12-11 14:46:03 -08001427 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001428 # This creates the per-test configuration object. It can be
1429 # overriden by subclasses, but should not need to be since it
1430 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001431 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001432 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001433 if hasattr(self, 'tenant_config_file'):
1434 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001435 git_path = os.path.join(
1436 os.path.dirname(
1437 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1438 'git')
1439 if os.path.exists(git_path):
1440 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001441 project = reponame.replace('_', '/')
1442 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001443 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001444 self.setupAllProjectKeys()
1445
1446 def setupAllProjectKeys(self):
1447 if self.create_project_keys:
1448 return
1449
1450 path = self.config.get('zuul', 'tenant_config')
1451 with open(os.path.join(FIXTURE_DIR, path)) as f:
1452 tenant_config = yaml.safe_load(f.read())
1453 for tenant in tenant_config:
1454 sources = tenant['tenant']['source']
1455 for source, conf in sources.items():
1456 for project in conf.get('config-repos', []):
1457 self.setupProjectKeys(source, project)
1458 for project in conf.get('project-repos', []):
1459 self.setupProjectKeys(source, project)
1460
1461 def setupProjectKeys(self, source, project):
1462 # Make sure we set up an RSA key for the project so that we
1463 # don't spend time generating one:
1464
1465 key_root = os.path.join(self.state_root, 'keys')
1466 if not os.path.isdir(key_root):
1467 os.mkdir(key_root, 0o700)
1468 private_key_file = os.path.join(key_root, source, project + '.pem')
1469 private_key_dir = os.path.dirname(private_key_file)
1470 self.log.debug("Installing test keys for project %s at %s" % (
1471 project, private_key_file))
1472 if not os.path.isdir(private_key_dir):
1473 os.makedirs(private_key_dir)
1474 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1475 with open(private_key_file, 'w') as o:
1476 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001477
James E. Blair498059b2016-12-20 13:50:13 -08001478 def setupZK(self):
1479 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001480 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001481 self.zk_chroot_fixture.zookeeper_host,
1482 self.zk_chroot_fixture.zookeeper_port,
1483 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001484
James E. Blair96c6bf82016-01-15 16:20:40 -08001485 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001486 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001487
1488 files = {}
1489 for (dirpath, dirnames, filenames) in os.walk(source_path):
1490 for filename in filenames:
1491 test_tree_filepath = os.path.join(dirpath, filename)
1492 common_path = os.path.commonprefix([test_tree_filepath,
1493 source_path])
1494 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1495 with open(test_tree_filepath, 'r') as f:
1496 content = f.read()
1497 files[relative_filepath] = content
1498 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001499 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001500
James E. Blaire18d4602017-01-05 11:17:28 -08001501 def assertNodepoolState(self):
1502 # Make sure that there are no pending requests
1503
1504 requests = self.fake_nodepool.getNodeRequests()
1505 self.assertEqual(len(requests), 0)
1506
1507 nodes = self.fake_nodepool.getNodes()
1508 for node in nodes:
1509 self.assertFalse(node['_lock'], "Node %s is locked" %
1510 (node['_oid'],))
1511
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001512 def assertNoGeneratedKeys(self):
1513 # Make sure that Zuul did not generate any project keys
1514 # (unless it was supposed to).
1515
1516 if self.create_project_keys:
1517 return
1518
1519 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1520 test_key = i.read()
1521
1522 key_root = os.path.join(self.state_root, 'keys')
1523 for root, dirname, files in os.walk(key_root):
1524 for fn in files:
1525 with open(os.path.join(root, fn)) as f:
1526 self.assertEqual(test_key, f.read())
1527
Clark Boylanb640e052014-04-03 16:41:46 -07001528 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001529 # Make sure that git.Repo objects have been garbage collected.
1530 repos = []
1531 gc.collect()
1532 for obj in gc.get_objects():
1533 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001534 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001535 repos.append(obj)
1536 self.assertEqual(len(repos), 0)
1537 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001538 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001539 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001540 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001541 for tenant in self.sched.abide.tenants.values():
1542 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001543 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001544 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001545
1546 def shutdown(self):
1547 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001548 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001549 self.merge_server.stop()
1550 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001551 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001552 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001553 self.sched.stop()
1554 self.sched.join()
1555 self.statsd.stop()
1556 self.statsd.join()
1557 self.webapp.stop()
1558 self.webapp.join()
1559 self.rpc.stop()
1560 self.rpc.join()
1561 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001562 self.fake_nodepool.stop()
1563 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001564 threads = threading.enumerate()
1565 if len(threads) > 1:
1566 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001567 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001568
1569 def init_repo(self, project):
1570 parts = project.split('/')
1571 path = os.path.join(self.upstream_root, *parts[:-1])
1572 if not os.path.exists(path):
1573 os.makedirs(path)
1574 path = os.path.join(self.upstream_root, project)
1575 repo = git.Repo.init(path)
1576
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001577 with repo.config_writer() as config_writer:
1578 config_writer.set_value('user', 'email', 'user@example.com')
1579 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001580
Clark Boylanb640e052014-04-03 16:41:46 -07001581 repo.index.commit('initial commit')
1582 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001583
James E. Blair97d902e2014-08-21 13:25:56 -07001584 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001585 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001586 repo.git.clean('-x', '-f', '-d')
1587
James E. Blair97d902e2014-08-21 13:25:56 -07001588 def create_branch(self, project, branch):
1589 path = os.path.join(self.upstream_root, project)
1590 repo = git.Repo.init(path)
1591 fn = os.path.join(path, 'README')
1592
1593 branch_head = repo.create_head(branch)
1594 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001595 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001596 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001597 f.close()
1598 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001599 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001600
James E. Blair97d902e2014-08-21 13:25:56 -07001601 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001602 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001603 repo.git.clean('-x', '-f', '-d')
1604
Sachi King9f16d522016-03-16 12:20:45 +11001605 def create_commit(self, project):
1606 path = os.path.join(self.upstream_root, project)
1607 repo = git.Repo(path)
1608 repo.head.reference = repo.heads['master']
1609 file_name = os.path.join(path, 'README')
1610 with open(file_name, 'a') as f:
1611 f.write('creating fake commit\n')
1612 repo.index.add([file_name])
1613 commit = repo.index.commit('Creating a fake commit')
1614 return commit.hexsha
1615
James E. Blairb8c16472015-05-05 14:55:26 -07001616 def orderedRelease(self):
1617 # Run one build at a time to ensure non-race order:
1618 while len(self.builds):
1619 self.release(self.builds[0])
1620 self.waitUntilSettled()
1621
Clark Boylanb640e052014-04-03 16:41:46 -07001622 def release(self, job):
1623 if isinstance(job, FakeBuild):
1624 job.release()
1625 else:
1626 job.waiting = False
1627 self.log.debug("Queued job %s released" % job.unique)
1628 self.gearman_server.wakeConnections()
1629
1630 def getParameter(self, job, name):
1631 if isinstance(job, FakeBuild):
1632 return job.parameters[name]
1633 else:
1634 parameters = json.loads(job.arguments)
1635 return parameters[name]
1636
Clark Boylanb640e052014-04-03 16:41:46 -07001637 def haveAllBuildsReported(self):
1638 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001639 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001640 return False
1641 # Find out if every build that the worker has completed has been
1642 # reported back to Zuul. If it hasn't then that means a Gearman
1643 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001644 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001645 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001646 if not zbuild:
1647 # It has already been reported
1648 continue
1649 # It hasn't been reported yet.
1650 return False
1651 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001652 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001653 if connection.state == 'GRAB_WAIT':
1654 return False
1655 return True
1656
1657 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001658 builds = self.executor_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001659 for build in builds:
1660 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001661 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001662 for j in conn.related_jobs.values():
1663 if j.unique == build.uuid:
1664 client_job = j
1665 break
1666 if not client_job:
1667 self.log.debug("%s is not known to the gearman client" %
1668 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001669 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001670 if not client_job.handle:
1671 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001672 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001673 server_job = self.gearman_server.jobs.get(client_job.handle)
1674 if not server_job:
1675 self.log.debug("%s is not known to the gearman server" %
1676 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001677 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001678 if not hasattr(server_job, 'waiting'):
1679 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001680 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001681 if server_job.waiting:
1682 continue
James E. Blair17302972016-08-10 16:11:42 -07001683 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001684 self.log.debug("%s has not reported start" % build)
1685 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001686 worker_build = self.executor_server.job_builds.get(
1687 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001688 if worker_build:
1689 if worker_build.isWaiting():
1690 continue
1691 else:
1692 self.log.debug("%s is running" % worker_build)
1693 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001694 else:
James E. Blair962220f2016-08-03 11:22:38 -07001695 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001696 return False
1697 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001698
James E. Blairdce6cea2016-12-20 16:45:32 -08001699 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001700 if self.fake_nodepool.paused:
1701 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001702 if self.sched.nodepool.requests:
1703 return False
1704 return True
1705
Jan Hruban6b71aff2015-10-22 16:58:08 +02001706 def eventQueuesEmpty(self):
1707 for queue in self.event_queues:
1708 yield queue.empty()
1709
1710 def eventQueuesJoin(self):
1711 for queue in self.event_queues:
1712 queue.join()
1713
Clark Boylanb640e052014-04-03 16:41:46 -07001714 def waitUntilSettled(self):
1715 self.log.debug("Waiting until settled...")
1716 start = time.time()
1717 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001718 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001719 self.log.error("Timeout waiting for Zuul to settle")
1720 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001721 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001722 self.log.error(" %s: %s" % (queue, queue.empty()))
1723 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001724 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001725 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001726 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001727 self.log.error("All requests completed: %s" %
1728 (self.areAllNodeRequestsComplete(),))
1729 self.log.error("Merge client jobs: %s" %
1730 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001731 raise Exception("Timeout waiting for Zuul to settle")
1732 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001733
Paul Belanger174a8272017-03-14 13:20:10 -04001734 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001735 # have all build states propogated to zuul?
1736 if self.haveAllBuildsReported():
1737 # Join ensures that the queue is empty _and_ events have been
1738 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001739 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001740 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001741 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001742 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001743 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001744 self.areAllNodeRequestsComplete() and
1745 all(self.eventQueuesEmpty())):
1746 # The queue empty check is placed at the end to
1747 # ensure that if a component adds an event between
1748 # when locked the run handler and checked that the
1749 # components were stable, we don't erroneously
1750 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001751 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001752 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001753 self.log.debug("...settled.")
1754 return
1755 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001756 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001757 self.sched.wake_event.wait(0.1)
1758
1759 def countJobResults(self, jobs, result):
1760 jobs = filter(lambda x: x.result == result, jobs)
1761 return len(jobs)
1762
James E. Blair96c6bf82016-01-15 16:20:40 -08001763 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001764 for job in self.history:
1765 if (job.name == name and
1766 (project is None or
1767 job.parameters['ZUUL_PROJECT'] == project)):
1768 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001769 raise Exception("Unable to find job %s in history" % name)
1770
1771 def assertEmptyQueues(self):
1772 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001773 for tenant in self.sched.abide.tenants.values():
1774 for pipeline in tenant.layout.pipelines.values():
1775 for queue in pipeline.queues:
1776 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001777 print('pipeline %s queue %s contents %s' % (
1778 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001779 self.assertEqual(len(queue.queue), 0,
1780 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001781
1782 def assertReportedStat(self, key, value=None, kind=None):
1783 start = time.time()
1784 while time.time() < (start + 5):
1785 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001786 k, v = stat.split(':')
1787 if key == k:
1788 if value is None and kind is None:
1789 return
1790 elif value:
1791 if value == v:
1792 return
1793 elif kind:
1794 if v.endswith('|' + kind):
1795 return
1796 time.sleep(0.1)
1797
Clark Boylanb640e052014-04-03 16:41:46 -07001798 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001799
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001800 def assertBuilds(self, builds):
1801 """Assert that the running builds are as described.
1802
1803 The list of running builds is examined and must match exactly
1804 the list of builds described by the input.
1805
1806 :arg list builds: A list of dictionaries. Each item in the
1807 list must match the corresponding build in the build
1808 history, and each element of the dictionary must match the
1809 corresponding attribute of the build.
1810
1811 """
James E. Blair3158e282016-08-19 09:34:11 -07001812 try:
1813 self.assertEqual(len(self.builds), len(builds))
1814 for i, d in enumerate(builds):
1815 for k, v in d.items():
1816 self.assertEqual(
1817 getattr(self.builds[i], k), v,
1818 "Element %i in builds does not match" % (i,))
1819 except Exception:
1820 for build in self.builds:
1821 self.log.error("Running build: %s" % build)
1822 else:
1823 self.log.error("No running builds")
1824 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001825
James E. Blairb536ecc2016-08-31 10:11:42 -07001826 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001827 """Assert that the completed builds are as described.
1828
1829 The list of completed builds is examined and must match
1830 exactly the list of builds described by the input.
1831
1832 :arg list history: A list of dictionaries. Each item in the
1833 list must match the corresponding build in the build
1834 history, and each element of the dictionary must match the
1835 corresponding attribute of the build.
1836
James E. Blairb536ecc2016-08-31 10:11:42 -07001837 :arg bool ordered: If true, the history must match the order
1838 supplied, if false, the builds are permitted to have
1839 arrived in any order.
1840
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001841 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001842 def matches(history_item, item):
1843 for k, v in item.items():
1844 if getattr(history_item, k) != v:
1845 return False
1846 return True
James E. Blair3158e282016-08-19 09:34:11 -07001847 try:
1848 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001849 if ordered:
1850 for i, d in enumerate(history):
1851 if not matches(self.history[i], d):
1852 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001853 "Element %i in history does not match %s" %
1854 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001855 else:
1856 unseen = self.history[:]
1857 for i, d in enumerate(history):
1858 found = False
1859 for unseen_item in unseen:
1860 if matches(unseen_item, d):
1861 found = True
1862 unseen.remove(unseen_item)
1863 break
1864 if not found:
1865 raise Exception("No match found for element %i "
1866 "in history" % (i,))
1867 if unseen:
1868 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001869 except Exception:
1870 for build in self.history:
1871 self.log.error("Completed build: %s" % build)
1872 else:
1873 self.log.error("No completed builds")
1874 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001875
James E. Blair6ac368c2016-12-22 18:07:20 -08001876 def printHistory(self):
1877 """Log the build history.
1878
1879 This can be useful during tests to summarize what jobs have
1880 completed.
1881
1882 """
1883 self.log.debug("Build history:")
1884 for build in self.history:
1885 self.log.debug(build)
1886
James E. Blair59fdbac2015-12-07 17:08:06 -08001887 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001888 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1889
1890 def updateConfigLayout(self, path):
1891 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001892 if not os.path.exists(root):
1893 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001894 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1895 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001896- tenant:
1897 name: openstack
1898 source:
1899 gerrit:
1900 config-repos:
1901 - %s
1902 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001903 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001904 self.config.set('zuul', 'tenant_config',
1905 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001906 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08001907
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001908 def addCommitToRepo(self, project, message, files,
1909 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001910 path = os.path.join(self.upstream_root, project)
1911 repo = git.Repo(path)
1912 repo.head.reference = branch
1913 zuul.merger.merger.reset_repo_to_head(repo)
1914 for fn, content in files.items():
1915 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001916 try:
1917 os.makedirs(os.path.dirname(fn))
1918 except OSError:
1919 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001920 with open(fn, 'w') as f:
1921 f.write(content)
1922 repo.index.add([fn])
1923 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001924 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001925 repo.heads[branch].commit = commit
1926 repo.head.reference = branch
1927 repo.git.clean('-x', '-f', '-d')
1928 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001929 if tag:
1930 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001931 return before
1932
1933 def commitLayoutUpdate(self, orig_name, source_name):
1934 source_path = os.path.join(self.test_root, 'upstream',
1935 source_name, 'zuul.yaml')
1936 with open(source_path, 'r') as nt:
1937 before = self.addCommitToRepo(
1938 orig_name, 'Pulling content from %s' % source_name,
1939 {'zuul.yaml': nt.read()})
1940 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001941
James E. Blair7fc8daa2016-08-08 15:37:15 -07001942 def addEvent(self, connection, event):
1943 """Inject a Fake (Gerrit) event.
1944
1945 This method accepts a JSON-encoded event and simulates Zuul
1946 having received it from Gerrit. It could (and should)
1947 eventually apply to any connection type, but is currently only
1948 used with Gerrit connections. The name of the connection is
1949 used to look up the corresponding server, and the event is
1950 simulated as having been received by all Zuul connections
1951 attached to that server. So if two Gerrit connections in Zuul
1952 are connected to the same Gerrit server, and you invoke this
1953 method specifying the name of one of them, the event will be
1954 received by both.
1955
1956 .. note::
1957
1958 "self.fake_gerrit.addEvent" calls should be migrated to
1959 this method.
1960
1961 :arg str connection: The name of the connection corresponding
1962 to the gerrit server.
1963 :arg str event: The JSON-encoded event.
1964
1965 """
1966 specified_conn = self.connections.connections[connection]
1967 for conn in self.connections.connections.values():
1968 if (isinstance(conn, specified_conn.__class__) and
1969 specified_conn.server == conn.server):
1970 conn.addEvent(event)
1971
James E. Blair3f876d52016-07-22 13:07:14 -07001972
1973class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04001974 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001975 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11001976
Joshua Heskethd78b4482015-09-14 16:56:34 -06001977
1978class ZuulDBTestCase(ZuulTestCase):
1979 def setup_config(self, config_file='zuul-connections-same-gerrit.conf'):
1980 super(ZuulDBTestCase, self).setup_config(config_file)
1981 for section_name in self.config.sections():
1982 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1983 section_name, re.I)
1984 if not con_match:
1985 continue
1986
1987 if self.config.get(section_name, 'driver') == 'sql':
1988 f = MySQLSchemaFixture()
1989 self.useFixture(f)
1990 if (self.config.get(section_name, 'dburi') ==
1991 '$MYSQL_FIXTURE_DBURI$'):
1992 self.config.set(section_name, 'dburi', f.dburi)