blob: 0221a9e89d63606578d69fb7c470b69fae51f43d [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
35import swiftclient
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
39import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
41
Clark Boylanb640e052014-04-03 16:41:46 -070042
43import git
44import gear
45import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080046import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080047import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060048import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070049import statsd
50import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080051import testtools.content
52import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080053from git.exc import NoSuchPathError
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
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100061import zuul.launcher.server
62import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080064import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070065import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070066import zuul.merger.merger
67import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070068import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080069import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070070
71FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
72 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080073
74KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070075
Clark Boylanb640e052014-04-03 16:41:46 -070076
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
James E. Blairc2a5ed72017-02-20 14:12:01 -0500271 def getChangeMergedEvent(self):
272 event = {"submitter": {"name": "Jenkins",
273 "username": "jenkins"},
274 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
275 "patchSet": self.patchsets[-1],
276 "change": self.data,
277 "type": "change-merged",
278 "eventCreatedOn": 1487613810}
279 return event
280
James E. Blair8cce42e2016-10-18 08:18:36 -0700281 def getRefUpdatedEvent(self):
282 path = os.path.join(self.upstream_root, self.project)
283 repo = git.Repo(path)
284 oldrev = repo.heads[self.branch].commit.hexsha
285
286 event = {
287 "type": "ref-updated",
288 "submitter": {
289 "name": "User Name",
290 },
291 "refUpdate": {
292 "oldRev": oldrev,
293 "newRev": self.patchsets[-1]['revision'],
294 "refName": self.branch,
295 "project": self.project,
296 }
297 }
298 return event
299
Joshua Hesketh642824b2014-07-01 17:54:59 +1000300 def addApproval(self, category, value, username='reviewer_john',
301 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700302 if not granted_on:
303 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000304 approval = {
305 'description': self.categories[category][0],
306 'type': category,
307 'value': str(value),
308 'by': {
309 'username': username,
310 'email': username + '@example.com',
311 },
312 'grantedOn': int(granted_on)
313 }
Clark Boylanb640e052014-04-03 16:41:46 -0700314 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
315 if x['by']['username'] == username and x['type'] == category:
316 del self.patchsets[-1]['approvals'][i]
317 self.patchsets[-1]['approvals'].append(approval)
318 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000319 'author': {'email': 'author@example.com',
320 'name': 'Patchset Author',
321 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700322 'change': {'branch': self.branch,
323 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
324 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000325 'owner': {'email': 'owner@example.com',
326 'name': 'Change Owner',
327 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700328 'project': self.project,
329 'subject': self.subject,
330 'topic': 'master',
331 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000332 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700333 'patchSet': self.patchsets[-1],
334 'type': 'comment-added'}
335 self.data['submitRecords'] = self.getSubmitRecords()
336 return json.loads(json.dumps(event))
337
338 def getSubmitRecords(self):
339 status = {}
340 for cat in self.categories.keys():
341 status[cat] = 0
342
343 for a in self.patchsets[-1]['approvals']:
344 cur = status[a['type']]
345 cat_min, cat_max = self.categories[a['type']][1:]
346 new = int(a['value'])
347 if new == cat_min:
348 cur = new
349 elif abs(new) > abs(cur):
350 cur = new
351 status[a['type']] = cur
352
353 labels = []
354 ok = True
355 for typ, cat in self.categories.items():
356 cur = status[typ]
357 cat_min, cat_max = cat[1:]
358 if cur == cat_min:
359 value = 'REJECT'
360 ok = False
361 elif cur == cat_max:
362 value = 'OK'
363 else:
364 value = 'NEED'
365 ok = False
366 labels.append({'label': cat[0], 'status': value})
367 if ok:
368 return [{'status': 'OK'}]
369 return [{'status': 'NOT_READY',
370 'labels': labels}]
371
372 def setDependsOn(self, other, patchset):
373 self.depends_on_change = other
374 d = {'id': other.data['id'],
375 'number': other.data['number'],
376 'ref': other.patchsets[patchset - 1]['ref']
377 }
378 self.data['dependsOn'] = [d]
379
380 other.needed_by_changes.append(self)
381 needed = other.data.get('neededBy', [])
382 d = {'id': self.data['id'],
383 'number': self.data['number'],
384 'ref': self.patchsets[patchset - 1]['ref'],
385 'revision': self.patchsets[patchset - 1]['revision']
386 }
387 needed.append(d)
388 other.data['neededBy'] = needed
389
390 def query(self):
391 self.queried += 1
392 d = self.data.get('dependsOn')
393 if d:
394 d = d[0]
395 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
396 d['isCurrentPatchSet'] = True
397 else:
398 d['isCurrentPatchSet'] = False
399 return json.loads(json.dumps(self.data))
400
401 def setMerged(self):
402 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000403 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700404 return
405 if self.fail_merge:
406 return
407 self.data['status'] = 'MERGED'
408 self.open = False
409
410 path = os.path.join(self.upstream_root, self.project)
411 repo = git.Repo(path)
412 repo.heads[self.branch].commit = \
413 repo.commit(self.patchsets[-1]['revision'])
414
415 def setReported(self):
416 self.reported += 1
417
418
James E. Blaire511d2f2016-12-08 15:22:26 -0800419class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700420 """A Fake Gerrit connection for use in tests.
421
422 This subclasses
423 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
424 ability for tests to add changes to the fake Gerrit it represents.
425 """
426
Joshua Hesketh352264b2015-08-11 23:42:08 +1000427 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700428
James E. Blaire511d2f2016-12-08 15:22:26 -0800429 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700430 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800431 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000432 connection_config)
433
James E. Blair7fc8daa2016-08-08 15:37:15 -0700434 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700435 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
436 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000437 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700438 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200439 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700440
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700441 def addFakeChange(self, project, branch, subject, status='NEW',
442 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700443 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700444 self.change_number += 1
445 c = FakeChange(self, self.change_number, project, branch, subject,
446 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700447 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700448 self.changes[self.change_number] = c
449 return c
450
Clark Boylanb640e052014-04-03 16:41:46 -0700451 def review(self, project, changeid, message, action):
452 number, ps = changeid.split(',')
453 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000454
455 # Add the approval back onto the change (ie simulate what gerrit would
456 # do).
457 # Usually when zuul leaves a review it'll create a feedback loop where
458 # zuul's review enters another gerrit event (which is then picked up by
459 # zuul). However, we can't mimic this behaviour (by adding this
460 # approval event into the queue) as it stops jobs from checking what
461 # happens before this event is triggered. If a job needs to see what
462 # happens they can add their own verified event into the queue.
463 # Nevertheless, we can update change with the new review in gerrit.
464
James E. Blair8b5408c2016-08-08 15:37:46 -0700465 for cat in action.keys():
466 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000468
James E. Blair8b5408c2016-08-08 15:37:46 -0700469 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000470 if 'label' in action:
471 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000472 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000473
Clark Boylanb640e052014-04-03 16:41:46 -0700474 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000475
Clark Boylanb640e052014-04-03 16:41:46 -0700476 if 'submit' in action:
477 change.setMerged()
478 if message:
479 change.setReported()
480
481 def query(self, number):
482 change = self.changes.get(int(number))
483 if change:
484 return change.query()
485 return {}
486
James E. Blairc494d542014-08-06 09:23:52 -0700487 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700488 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700489 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800490 if query.startswith('change:'):
491 # Query a specific changeid
492 changeid = query[len('change:'):]
493 l = [change.query() for change in self.changes.values()
494 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700495 elif query.startswith('message:'):
496 # Query the content of a commit message
497 msg = query[len('message:'):].strip()
498 l = [change.query() for change in self.changes.values()
499 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800500 else:
501 # Query all open changes
502 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700503 return l
James E. Blairc494d542014-08-06 09:23:52 -0700504
Joshua Hesketh352264b2015-08-11 23:42:08 +1000505 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700506 pass
507
Joshua Hesketh352264b2015-08-11 23:42:08 +1000508 def getGitUrl(self, project):
509 return os.path.join(self.upstream_root, project.name)
510
Clark Boylanb640e052014-04-03 16:41:46 -0700511
512class BuildHistory(object):
513 def __init__(self, **kw):
514 self.__dict__.update(kw)
515
516 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700517 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
518 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700519
520
521class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200522 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700523 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700524 self.url = url
525
526 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700527 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700528 path = res.path
529 project = '/'.join(path.split('/')[2:-2])
530 ret = '001e# service=git-upload-pack\n'
531 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
532 'multi_ack thin-pack side-band side-band-64k ofs-delta '
533 'shallow no-progress include-tag multi_ack_detailed no-done\n')
534 path = os.path.join(self.upstream_root, project)
535 repo = git.Repo(path)
536 for ref in repo.refs:
537 r = ref.object.hexsha + ' ' + ref.path + '\n'
538 ret += '%04x%s' % (len(r) + 4, r)
539 ret += '0000'
540 return ret
541
542
Clark Boylanb640e052014-04-03 16:41:46 -0700543class FakeStatsd(threading.Thread):
544 def __init__(self):
545 threading.Thread.__init__(self)
546 self.daemon = True
547 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
548 self.sock.bind(('', 0))
549 self.port = self.sock.getsockname()[1]
550 self.wake_read, self.wake_write = os.pipe()
551 self.stats = []
552
553 def run(self):
554 while True:
555 poll = select.poll()
556 poll.register(self.sock, select.POLLIN)
557 poll.register(self.wake_read, select.POLLIN)
558 ret = poll.poll()
559 for (fd, event) in ret:
560 if fd == self.sock.fileno():
561 data = self.sock.recvfrom(1024)
562 if not data:
563 return
564 self.stats.append(data[0])
565 if fd == self.wake_read:
566 return
567
568 def stop(self):
569 os.write(self.wake_write, '1\n')
570
571
James E. Blaire1767bc2016-08-02 10:00:27 -0700572class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700573 log = logging.getLogger("zuul.test")
574
James E. Blair34776ee2016-08-25 13:53:54 -0700575 def __init__(self, launch_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700576 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700577 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700578 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700579 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700580 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700581 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700582 # TODOv3(jeblair): self.node is really "the image of the node
583 # assigned". We should rename it (self.node_image?) if we
584 # keep using it like this, or we may end up exposing more of
585 # the complexity around multi-node jobs here
586 # (self.nodes[0].image?)
587 self.node = None
588 if len(self.parameters.get('nodes')) == 1:
589 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700590 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100591 self.pipeline = self.parameters['ZUUL_PIPELINE']
592 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700593 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700594 self.wait_condition = threading.Condition()
595 self.waiting = False
596 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500597 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700598 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700599 self.changes = None
600 if 'ZUUL_CHANGE_IDS' in self.parameters:
601 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700602
James E. Blair3158e282016-08-19 09:34:11 -0700603 def __repr__(self):
604 waiting = ''
605 if self.waiting:
606 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100607 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
608 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700609
Clark Boylanb640e052014-04-03 16:41:46 -0700610 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700611 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700612 self.wait_condition.acquire()
613 self.wait_condition.notify()
614 self.waiting = False
615 self.log.debug("Build %s released" % self.unique)
616 self.wait_condition.release()
617
618 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700619 """Return whether this build is being held.
620
621 :returns: Whether the build is being held.
622 :rtype: bool
623 """
624
Clark Boylanb640e052014-04-03 16:41:46 -0700625 self.wait_condition.acquire()
626 if self.waiting:
627 ret = True
628 else:
629 ret = False
630 self.wait_condition.release()
631 return ret
632
633 def _wait(self):
634 self.wait_condition.acquire()
635 self.waiting = True
636 self.log.debug("Build %s waiting" % self.unique)
637 self.wait_condition.wait()
638 self.wait_condition.release()
639
640 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700641 self.log.debug('Running build %s' % self.unique)
642
James E. Blaire1767bc2016-08-02 10:00:27 -0700643 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700644 self.log.debug('Holding build %s' % self.unique)
645 self._wait()
646 self.log.debug("Build %s continuing" % self.unique)
647
James E. Blair412fba82017-01-26 15:00:50 -0800648 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700649 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800650 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700651 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800652 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500653 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800654 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700655
James E. Blaire1767bc2016-08-02 10:00:27 -0700656 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700657
James E. Blaira5dba232016-08-08 15:53:24 -0700658 def shouldFail(self):
659 changes = self.launch_server.fail_tests.get(self.name, [])
660 for change in changes:
661 if self.hasChanges(change):
662 return True
663 return False
664
James E. Blaire7b99a02016-08-05 14:27:34 -0700665 def hasChanges(self, *changes):
666 """Return whether this build has certain changes in its git repos.
667
668 :arg FakeChange changes: One or more changes (varargs) that
669 are expected to be present (in order) in the git repository of
670 the active project.
671
672 :returns: Whether the build has the indicated changes.
673 :rtype: bool
674
675 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800676 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500677 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800678 try:
679 repo = git.Repo(path)
680 except NoSuchPathError as e:
681 self.log.debug('%s' % e)
682 return False
683 ref = self.parameters['ZUUL_REF']
684 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
685 commit_message = '%s-1' % change.subject
686 self.log.debug("Checking if build %s has changes; commit_message "
687 "%s; repo_messages %s" % (self, commit_message,
688 repo_messages))
689 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700690 self.log.debug(" messages do not match")
691 return False
692 self.log.debug(" OK")
693 return True
694
Clark Boylanb640e052014-04-03 16:41:46 -0700695
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000696class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blaire7b99a02016-08-05 14:27:34 -0700697 """An Ansible launcher to be used in tests.
698
699 :ivar bool hold_jobs_in_build: If true, when jobs are launched
700 they will report that they have started but then pause until
701 released before reporting completion. This attribute may be
702 changed at any time and will take effect for subsequently
703 launched builds, but previously held builds will still need to
704 be explicitly released.
705
706 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800707 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700708 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800709 self._test_root = kw.pop('_test_root', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800710 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700711 self.hold_jobs_in_build = False
712 self.lock = threading.Lock()
713 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700714 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700715 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700716 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800717
James E. Blaira5dba232016-08-08 15:53:24 -0700718 def failJob(self, name, change):
James E. Blaire7b99a02016-08-05 14:27:34 -0700719 """Instruct the launcher to report matching builds as failures.
720
721 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700722 :arg Change change: The :py:class:`~tests.base.FakeChange`
723 instance which should cause the job to fail. This job
724 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700725
726 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700727 l = self.fail_tests.get(name, [])
728 l.append(change)
729 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800730
James E. Blair962220f2016-08-03 11:22:38 -0700731 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700732 """Release a held build.
733
734 :arg str regex: A regular expression which, if supplied, will
735 cause only builds with matching names to be released. If
736 not supplied, all builds will be released.
737
738 """
James E. Blair962220f2016-08-03 11:22:38 -0700739 builds = self.running_builds[:]
740 self.log.debug("Releasing build %s (%s)" % (regex,
741 len(self.running_builds)))
742 for build in builds:
743 if not regex or re.match(regex, build.name):
744 self.log.debug("Releasing build %s" %
745 (build.parameters['ZUUL_UUID']))
746 build.release()
747 else:
748 self.log.debug("Not releasing build %s" %
749 (build.parameters['ZUUL_UUID']))
750 self.log.debug("Done releasing builds %s (%s)" %
751 (regex, len(self.running_builds)))
752
James E. Blair17302972016-08-10 16:11:42 -0700753 def launchJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700754 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700755 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700756 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700757 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800758 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500759 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800760 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100761 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
762 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700763
764 def stopJob(self, job):
765 self.log.debug("handle stop")
766 parameters = json.loads(job.arguments)
767 uuid = parameters['uuid']
768 for build in self.running_builds:
769 if build.unique == uuid:
770 build.aborted = True
771 build.release()
772 super(RecordingLaunchServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700773
Joshua Hesketh50c21782016-10-13 21:34:14 +1100774
775class RecordingAnsibleJob(zuul.launcher.server.AnsibleJob):
Paul Belanger96618ed2017-03-01 09:42:33 -0500776 def runPlaybooks(self, args):
Joshua Hesketh50c21782016-10-13 21:34:14 +1100777 build = self.launcher_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800778 build.jobdir = self.jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700779
Paul Belanger96618ed2017-03-01 09:42:33 -0500780 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
James E. Blair412fba82017-01-26 15:00:50 -0800781
Joshua Hesketh50c21782016-10-13 21:34:14 +1100782 self.launcher_server.lock.acquire()
783 self.launcher_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700784 BuildHistory(name=build.name, result=result, changes=build.changes,
785 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800786 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700787 pipeline=build.parameters['ZUUL_PIPELINE'])
788 )
Joshua Hesketh50c21782016-10-13 21:34:14 +1100789 self.launcher_server.running_builds.remove(build)
790 del self.launcher_server.job_builds[self.job.unique]
791 self.launcher_server.lock.release()
James E. Blair412fba82017-01-26 15:00:50 -0800792 return result
793
Monty Taylore6562aa2017-02-20 07:37:39 -0500794 def runAnsible(self, cmd, timeout, trusted=False):
James E. Blair412fba82017-01-26 15:00:50 -0800795 build = self.launcher_server.job_builds[self.job.unique]
796
797 if self.launcher_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600798 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500799 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800800 else:
801 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700802 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800803
James E. Blairad8dca02017-02-21 11:48:32 -0500804 def getHostList(self, args):
805 self.log.debug("hostlist")
806 hosts = super(RecordingAnsibleJob, self).getHostList(args)
807 for name, d in hosts:
808 d['ansible_connection'] = 'local'
809 hosts.append(('localhost', dict(ansible_connection='local')))
810 return hosts
811
James E. Blairf5dbd002015-12-23 15:26:17 -0800812
Clark Boylanb640e052014-04-03 16:41:46 -0700813class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700814 """A Gearman server for use in tests.
815
816 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
817 added to the queue but will not be distributed to workers
818 until released. This attribute may be changed at any time and
819 will take effect for subsequently enqueued jobs, but
820 previously held jobs will still need to be explicitly
821 released.
822
823 """
824
Clark Boylanb640e052014-04-03 16:41:46 -0700825 def __init__(self):
826 self.hold_jobs_in_queue = False
827 super(FakeGearmanServer, self).__init__(0)
828
829 def getJobForConnection(self, connection, peek=False):
830 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
831 for job in queue:
832 if not hasattr(job, 'waiting'):
Paul Belanger6ab6af72016-11-06 11:32:59 -0500833 if job.name.startswith('launcher:launch'):
Clark Boylanb640e052014-04-03 16:41:46 -0700834 job.waiting = self.hold_jobs_in_queue
835 else:
836 job.waiting = False
837 if job.waiting:
838 continue
839 if job.name in connection.functions:
840 if not peek:
841 queue.remove(job)
842 connection.related_jobs[job.handle] = job
843 job.worker_connection = connection
844 job.running = True
845 return job
846 return None
847
848 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700849 """Release a held job.
850
851 :arg str regex: A regular expression which, if supplied, will
852 cause only jobs with matching names to be released. If
853 not supplied, all jobs will be released.
854 """
Clark Boylanb640e052014-04-03 16:41:46 -0700855 released = False
856 qlen = (len(self.high_queue) + len(self.normal_queue) +
857 len(self.low_queue))
858 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
859 for job in self.getQueue():
Paul Belanger6ab6af72016-11-06 11:32:59 -0500860 if job.name != 'launcher:launch':
Clark Boylanb640e052014-04-03 16:41:46 -0700861 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500862 parameters = json.loads(job.arguments)
863 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700864 self.log.debug("releasing queued job %s" %
865 job.unique)
866 job.waiting = False
867 released = True
868 else:
869 self.log.debug("not releasing queued job %s" %
870 job.unique)
871 if released:
872 self.wakeConnections()
873 qlen = (len(self.high_queue) + len(self.normal_queue) +
874 len(self.low_queue))
875 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
876
877
878class FakeSMTP(object):
879 log = logging.getLogger('zuul.FakeSMTP')
880
881 def __init__(self, messages, server, port):
882 self.server = server
883 self.port = port
884 self.messages = messages
885
886 def sendmail(self, from_email, to_email, msg):
887 self.log.info("Sending email from %s, to %s, with msg %s" % (
888 from_email, to_email, msg))
889
890 headers = msg.split('\n\n', 1)[0]
891 body = msg.split('\n\n', 1)[1]
892
893 self.messages.append(dict(
894 from_email=from_email,
895 to_email=to_email,
896 msg=msg,
897 headers=headers,
898 body=body,
899 ))
900
901 return True
902
903 def quit(self):
904 return True
905
906
907class FakeSwiftClientConnection(swiftclient.client.Connection):
908 def post_account(self, headers):
909 # Do nothing
910 pass
911
912 def get_auth(self):
913 # Returns endpoint and (unused) auth token
914 endpoint = os.path.join('https://storage.example.org', 'V1',
915 'AUTH_account')
916 return endpoint, ''
917
918
James E. Blairdce6cea2016-12-20 16:45:32 -0800919class FakeNodepool(object):
920 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800921 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800922
923 log = logging.getLogger("zuul.test.FakeNodepool")
924
925 def __init__(self, host, port, chroot):
926 self.client = kazoo.client.KazooClient(
927 hosts='%s:%s%s' % (host, port, chroot))
928 self.client.start()
929 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800930 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800931 self.thread = threading.Thread(target=self.run)
932 self.thread.daemon = True
933 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800934 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800935
936 def stop(self):
937 self._running = False
938 self.thread.join()
939 self.client.stop()
940 self.client.close()
941
942 def run(self):
943 while self._running:
944 self._run()
945 time.sleep(0.1)
946
947 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800948 if self.paused:
949 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800950 for req in self.getNodeRequests():
951 self.fulfillRequest(req)
952
953 def getNodeRequests(self):
954 try:
955 reqids = self.client.get_children(self.REQUEST_ROOT)
956 except kazoo.exceptions.NoNodeError:
957 return []
958 reqs = []
959 for oid in sorted(reqids):
960 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800961 try:
962 data, stat = self.client.get(path)
963 data = json.loads(data)
964 data['_oid'] = oid
965 reqs.append(data)
966 except kazoo.exceptions.NoNodeError:
967 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800968 return reqs
969
James E. Blaire18d4602017-01-05 11:17:28 -0800970 def getNodes(self):
971 try:
972 nodeids = self.client.get_children(self.NODE_ROOT)
973 except kazoo.exceptions.NoNodeError:
974 return []
975 nodes = []
976 for oid in sorted(nodeids):
977 path = self.NODE_ROOT + '/' + oid
978 data, stat = self.client.get(path)
979 data = json.loads(data)
980 data['_oid'] = oid
981 try:
982 lockfiles = self.client.get_children(path + '/lock')
983 except kazoo.exceptions.NoNodeError:
984 lockfiles = []
985 if lockfiles:
986 data['_lock'] = True
987 else:
988 data['_lock'] = False
989 nodes.append(data)
990 return nodes
991
James E. Blaira38c28e2017-01-04 10:33:20 -0800992 def makeNode(self, request_id, node_type):
993 now = time.time()
994 path = '/nodepool/nodes/'
995 data = dict(type=node_type,
996 provider='test-provider',
997 region='test-region',
998 az=None,
999 public_ipv4='127.0.0.1',
1000 private_ipv4=None,
1001 public_ipv6=None,
1002 allocated_to=request_id,
1003 state='ready',
1004 state_time=now,
1005 created_time=now,
1006 updated_time=now,
1007 image_id=None,
1008 launcher='fake-nodepool')
1009 data = json.dumps(data)
1010 path = self.client.create(path, data,
1011 makepath=True,
1012 sequence=True)
1013 nodeid = path.split("/")[-1]
1014 return nodeid
1015
James E. Blair6ab79e02017-01-06 10:10:17 -08001016 def addFailRequest(self, request):
1017 self.fail_requests.add(request['_oid'])
1018
James E. Blairdce6cea2016-12-20 16:45:32 -08001019 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001020 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001021 return
1022 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001023 oid = request['_oid']
1024 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001025
James E. Blair6ab79e02017-01-06 10:10:17 -08001026 if oid in self.fail_requests:
1027 request['state'] = 'failed'
1028 else:
1029 request['state'] = 'fulfilled'
1030 nodes = []
1031 for node in request['node_types']:
1032 nodeid = self.makeNode(oid, node)
1033 nodes.append(nodeid)
1034 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001035
James E. Blaira38c28e2017-01-04 10:33:20 -08001036 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001037 path = self.REQUEST_ROOT + '/' + oid
1038 data = json.dumps(request)
1039 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1040 self.client.set(path, data)
1041
1042
James E. Blair498059b2016-12-20 13:50:13 -08001043class ChrootedKazooFixture(fixtures.Fixture):
1044 def __init__(self):
1045 super(ChrootedKazooFixture, self).__init__()
1046
1047 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1048 if ':' in zk_host:
1049 host, port = zk_host.split(':')
1050 else:
1051 host = zk_host
1052 port = None
1053
1054 self.zookeeper_host = host
1055
1056 if not port:
1057 self.zookeeper_port = 2181
1058 else:
1059 self.zookeeper_port = int(port)
1060
1061 def _setUp(self):
1062 # Make sure the test chroot paths do not conflict
1063 random_bits = ''.join(random.choice(string.ascii_lowercase +
1064 string.ascii_uppercase)
1065 for x in range(8))
1066
1067 rand_test_path = '%s_%s' % (random_bits, os.getpid())
1068 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1069
1070 # Ensure the chroot path exists and clean up any pre-existing znodes.
1071 _tmp_client = kazoo.client.KazooClient(
1072 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1073 _tmp_client.start()
1074
1075 if _tmp_client.exists(self.zookeeper_chroot):
1076 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1077
1078 _tmp_client.ensure_path(self.zookeeper_chroot)
1079 _tmp_client.stop()
1080 _tmp_client.close()
1081
1082 self.addCleanup(self._cleanup)
1083
1084 def _cleanup(self):
1085 '''Remove the chroot path.'''
1086 # Need a non-chroot'ed client to remove the chroot path
1087 _tmp_client = kazoo.client.KazooClient(
1088 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1089 _tmp_client.start()
1090 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1091 _tmp_client.stop()
1092
1093
Joshua Heskethd78b4482015-09-14 16:56:34 -06001094class MySQLSchemaFixture(fixtures.Fixture):
1095 def setUp(self):
1096 super(MySQLSchemaFixture, self).setUp()
1097
1098 random_bits = ''.join(random.choice(string.ascii_lowercase +
1099 string.ascii_uppercase)
1100 for x in range(8))
1101 self.name = '%s_%s' % (random_bits, os.getpid())
1102 self.passwd = uuid.uuid4().hex
1103 db = pymysql.connect(host="localhost",
1104 user="openstack_citest",
1105 passwd="openstack_citest",
1106 db="openstack_citest")
1107 cur = db.cursor()
1108 cur.execute("create database %s" % self.name)
1109 cur.execute(
1110 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1111 (self.name, self.name, self.passwd))
1112 cur.execute("flush privileges")
1113
1114 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1115 self.passwd,
1116 self.name)
1117 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1118 self.addCleanup(self.cleanup)
1119
1120 def cleanup(self):
1121 db = pymysql.connect(host="localhost",
1122 user="openstack_citest",
1123 passwd="openstack_citest",
1124 db="openstack_citest")
1125 cur = db.cursor()
1126 cur.execute("drop database %s" % self.name)
1127 cur.execute("drop user '%s'@'localhost'" % self.name)
1128 cur.execute("flush privileges")
1129
1130
Maru Newby3fe5f852015-01-13 04:22:14 +00001131class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001132 log = logging.getLogger("zuul.test")
Clint Byruma9626572017-02-22 14:04:00 -05001133 wait_timeout = 20
Clark Boylanb640e052014-04-03 16:41:46 -07001134
James E. Blair1c236df2017-02-01 14:07:24 -08001135 def attachLogs(self, *args):
1136 def reader():
1137 self._log_stream.seek(0)
1138 while True:
1139 x = self._log_stream.read(4096)
1140 if not x:
1141 break
1142 yield x.encode('utf8')
1143 content = testtools.content.content_from_reader(
1144 reader,
1145 testtools.content_type.UTF8_TEXT,
1146 False)
1147 self.addDetail('logging', content)
1148
Clark Boylanb640e052014-04-03 16:41:46 -07001149 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001150 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001151 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1152 try:
1153 test_timeout = int(test_timeout)
1154 except ValueError:
1155 # If timeout value is invalid do not set a timeout.
1156 test_timeout = 0
1157 if test_timeout > 0:
1158 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1159
1160 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1161 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1162 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1163 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1164 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1165 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1166 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1167 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1168 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1169 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001170 self._log_stream = StringIO()
1171 self.addOnException(self.attachLogs)
1172 else:
1173 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001174
James E. Blair1c236df2017-02-01 14:07:24 -08001175 handler = logging.StreamHandler(self._log_stream)
1176 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1177 '%(levelname)-8s %(message)s')
1178 handler.setFormatter(formatter)
1179
1180 logger = logging.getLogger()
1181 logger.setLevel(logging.DEBUG)
1182 logger.addHandler(handler)
1183
1184 # NOTE(notmorgan): Extract logging overrides for specific
1185 # libraries from the OS_LOG_DEFAULTS env and create loggers
1186 # for each. This is used to limit the output during test runs
1187 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001188 log_defaults_from_env = os.environ.get(
1189 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001190 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001191
James E. Blairdce6cea2016-12-20 16:45:32 -08001192 if log_defaults_from_env:
1193 for default in log_defaults_from_env.split(','):
1194 try:
1195 name, level_str = default.split('=', 1)
1196 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001197 logger = logging.getLogger(name)
1198 logger.setLevel(level)
1199 logger.addHandler(handler)
1200 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001201 except ValueError:
1202 # NOTE(notmorgan): Invalid format of the log default,
1203 # skip and don't try and apply a logger for the
1204 # specified module
1205 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001206
Maru Newby3fe5f852015-01-13 04:22:14 +00001207
1208class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001209 """A test case with a functioning Zuul.
1210
1211 The following class variables are used during test setup and can
1212 be overidden by subclasses but are effectively read-only once a
1213 test method starts running:
1214
1215 :cvar str config_file: This points to the main zuul config file
1216 within the fixtures directory. Subclasses may override this
1217 to obtain a different behavior.
1218
1219 :cvar str tenant_config_file: This is the tenant config file
1220 (which specifies from what git repos the configuration should
1221 be loaded). It defaults to the value specified in
1222 `config_file` but can be overidden by subclasses to obtain a
1223 different tenant/project layout while using the standard main
1224 configuration.
1225
1226 The following are instance variables that are useful within test
1227 methods:
1228
1229 :ivar FakeGerritConnection fake_<connection>:
1230 A :py:class:`~tests.base.FakeGerritConnection` will be
1231 instantiated for each connection present in the config file
1232 and stored here. For instance, `fake_gerrit` will hold the
1233 FakeGerritConnection object for a connection named `gerrit`.
1234
1235 :ivar FakeGearmanServer gearman_server: An instance of
1236 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1237 server that all of the Zuul components in this test use to
1238 communicate with each other.
1239
1240 :ivar RecordingLaunchServer launch_server: An instance of
1241 :py:class:`~tests.base.RecordingLaunchServer` which is the
1242 Ansible launch server used to run jobs for this test.
1243
1244 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1245 representing currently running builds. They are appended to
1246 the list in the order they are launched, and removed from this
1247 list upon completion.
1248
1249 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1250 objects representing completed builds. They are appended to
1251 the list in the order they complete.
1252
1253 """
1254
James E. Blair83005782015-12-11 14:46:03 -08001255 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001256 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -07001257
1258 def _startMerger(self):
1259 self.merge_server = zuul.merger.server.MergeServer(self.config,
1260 self.connections)
1261 self.merge_server.start()
1262
Maru Newby3fe5f852015-01-13 04:22:14 +00001263 def setUp(self):
1264 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001265
1266 self.setupZK()
1267
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001268 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001269 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001270 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1271 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001272 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001273 tmp_root = tempfile.mkdtemp(
1274 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001275 self.test_root = os.path.join(tmp_root, "zuul-test")
1276 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001277 self.merger_src_root = os.path.join(self.test_root, "merger-git")
1278 self.launcher_src_root = os.path.join(self.test_root, "launcher-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001279 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001280
1281 if os.path.exists(self.test_root):
1282 shutil.rmtree(self.test_root)
1283 os.makedirs(self.test_root)
1284 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001285 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001286
1287 # Make per test copy of Configuration.
1288 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001289 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001290 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001291 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001292 self.config.set('merger', 'git_dir', self.merger_src_root)
1293 self.config.set('launcher', 'git_dir', self.launcher_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001294 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001295
1296 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001297 # TODOv3(jeblair): remove these and replace with new git
1298 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001299 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -07001300 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001301 self.init_repo("org/project5")
1302 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001303 self.init_repo("org/one-job-project")
1304 self.init_repo("org/nonvoting-project")
1305 self.init_repo("org/templated-project")
1306 self.init_repo("org/layered-project")
1307 self.init_repo("org/node-project")
1308 self.init_repo("org/conflict-project")
1309 self.init_repo("org/noop-project")
1310 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +00001311 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001312
1313 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001314 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1315 # see: https://github.com/jsocol/pystatsd/issues/61
1316 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001317 os.environ['STATSD_PORT'] = str(self.statsd.port)
1318 self.statsd.start()
1319 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001320 reload_module(statsd)
1321 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001322
1323 self.gearman_server = FakeGearmanServer()
1324
1325 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001326 self.log.info("Gearman server on port %s" %
1327 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001328
James E. Blaire511d2f2016-12-08 15:22:26 -08001329 gerritsource.GerritSource.replication_timeout = 1.5
1330 gerritsource.GerritSource.replication_retry_interval = 0.5
1331 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001332
Joshua Hesketh352264b2015-08-11 23:42:08 +10001333 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001334
1335 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
1336 FakeSwiftClientConnection))
James E. Blaire511d2f2016-12-08 15:22:26 -08001337
Clark Boylanb640e052014-04-03 16:41:46 -07001338 self.swift = zuul.lib.swift.Swift(self.config)
1339
Jan Hruban6b71aff2015-10-22 16:58:08 +02001340 self.event_queues = [
1341 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001342 self.sched.trigger_event_queue,
1343 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001344 ]
1345
James E. Blairfef78942016-03-11 16:28:56 -08001346 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001347 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001348
Clark Boylanb640e052014-04-03 16:41:46 -07001349 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001350 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001351 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001352 return FakeURLOpener(self.upstream_root, *args, **kw)
1353
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001354 old_urlopen = urllib.request.urlopen
1355 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001356
James E. Blair3f876d52016-07-22 13:07:14 -07001357 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001358
James E. Blaire1767bc2016-08-02 10:00:27 -07001359 self.launch_server = RecordingLaunchServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001360 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001361 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001362 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001363 _test_root=self.test_root,
1364 keep_jobdir=KEEP_TEMPDIRS)
James E. Blaire1767bc2016-08-02 10:00:27 -07001365 self.launch_server.start()
1366 self.history = self.launch_server.build_history
1367 self.builds = self.launch_server.running_builds
1368
1369 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001370 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001371 self.merge_client = zuul.merger.client.MergeClient(
1372 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001373 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001374 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001375 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001376
James E. Blair0d5a36e2017-02-21 10:53:44 -05001377 self.fake_nodepool = FakeNodepool(
1378 self.zk_chroot_fixture.zookeeper_host,
1379 self.zk_chroot_fixture.zookeeper_port,
1380 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001381
James E. Blaire1767bc2016-08-02 10:00:27 -07001382 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001383 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001384 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001385 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001386
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001387 self.webapp = zuul.webapp.WebApp(
1388 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001389 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001390
1391 self.sched.start()
1392 self.sched.reconfigure(self.config)
1393 self.sched.resume()
1394 self.webapp.start()
1395 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001396 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001397
Clark Boylanb640e052014-04-03 16:41:46 -07001398 self.addCleanup(self.shutdown)
1399
James E. Blaire18d4602017-01-05 11:17:28 -08001400 def tearDown(self):
1401 super(ZuulTestCase, self).tearDown()
1402 self.assertFinalState()
1403
James E. Blairfef78942016-03-11 16:28:56 -08001404 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001405 # Set up gerrit related fakes
1406 # Set a changes database so multiple FakeGerrit's can report back to
1407 # a virtual canonical database given by the configured hostname
1408 self.gerrit_changes_dbs = {}
1409
1410 def getGerritConnection(driver, name, config):
1411 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1412 con = FakeGerritConnection(driver, name, config,
1413 changes_db=db,
1414 upstream_root=self.upstream_root)
1415 self.event_queues.append(con.event_queue)
1416 setattr(self, 'fake_' + name, con)
1417 return con
1418
1419 self.useFixture(fixtures.MonkeyPatch(
1420 'zuul.driver.gerrit.GerritDriver.getConnection',
1421 getGerritConnection))
1422
1423 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001424 # TODO(jhesketh): This should come from lib.connections for better
1425 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001426 # Register connections from the config
1427 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001428
Joshua Hesketh352264b2015-08-11 23:42:08 +10001429 def FakeSMTPFactory(*args, **kw):
1430 args = [self.smtp_messages] + list(args)
1431 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001432
Joshua Hesketh352264b2015-08-11 23:42:08 +10001433 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001434
James E. Blaire511d2f2016-12-08 15:22:26 -08001435 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001436 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001437 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001438
James E. Blair83005782015-12-11 14:46:03 -08001439 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001440 # This creates the per-test configuration object. It can be
1441 # overriden by subclasses, but should not need to be since it
1442 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001443 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001444 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001445 if hasattr(self, 'tenant_config_file'):
1446 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001447 git_path = os.path.join(
1448 os.path.dirname(
1449 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1450 'git')
1451 if os.path.exists(git_path):
1452 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001453 project = reponame.replace('_', '/')
1454 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001455 os.path.join(git_path, reponame))
1456
James E. Blair498059b2016-12-20 13:50:13 -08001457 def setupZK(self):
1458 self.zk_chroot_fixture = self.useFixture(ChrootedKazooFixture())
James E. Blair0d5a36e2017-02-21 10:53:44 -05001459 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001460 self.zk_chroot_fixture.zookeeper_host,
1461 self.zk_chroot_fixture.zookeeper_port,
1462 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001463
James E. Blair96c6bf82016-01-15 16:20:40 -08001464 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001465 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001466
1467 files = {}
1468 for (dirpath, dirnames, filenames) in os.walk(source_path):
1469 for filename in filenames:
1470 test_tree_filepath = os.path.join(dirpath, filename)
1471 common_path = os.path.commonprefix([test_tree_filepath,
1472 source_path])
1473 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1474 with open(test_tree_filepath, 'r') as f:
1475 content = f.read()
1476 files[relative_filepath] = content
1477 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001478 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001479
James E. Blaire18d4602017-01-05 11:17:28 -08001480 def assertNodepoolState(self):
1481 # Make sure that there are no pending requests
1482
1483 requests = self.fake_nodepool.getNodeRequests()
1484 self.assertEqual(len(requests), 0)
1485
1486 nodes = self.fake_nodepool.getNodes()
1487 for node in nodes:
1488 self.assertFalse(node['_lock'], "Node %s is locked" %
1489 (node['_oid'],))
1490
Clark Boylanb640e052014-04-03 16:41:46 -07001491 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001492 # Make sure that git.Repo objects have been garbage collected.
1493 repos = []
1494 gc.collect()
1495 for obj in gc.get_objects():
1496 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001497 self.log.debug("Leaked git repo object: %s" % repr(obj))
1498 for r in gc.get_referrers(obj):
1499 self.log.debug(" referrer: %s" % repr(r))
Clark Boylanb640e052014-04-03 16:41:46 -07001500 repos.append(obj)
1501 self.assertEqual(len(repos), 0)
1502 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001503 self.assertNodepoolState()
James E. Blair83005782015-12-11 14:46:03 -08001504 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001505 for tenant in self.sched.abide.tenants.values():
1506 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001507 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001508 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001509
1510 def shutdown(self):
1511 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001512 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001513 self.merge_server.stop()
1514 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001515 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001516 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001517 self.sched.stop()
1518 self.sched.join()
1519 self.statsd.stop()
1520 self.statsd.join()
1521 self.webapp.stop()
1522 self.webapp.join()
1523 self.rpc.stop()
1524 self.rpc.join()
1525 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001526 self.fake_nodepool.stop()
1527 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001528 threads = threading.enumerate()
1529 if len(threads) > 1:
1530 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001531 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001532
1533 def init_repo(self, project):
1534 parts = project.split('/')
1535 path = os.path.join(self.upstream_root, *parts[:-1])
1536 if not os.path.exists(path):
1537 os.makedirs(path)
1538 path = os.path.join(self.upstream_root, project)
1539 repo = git.Repo.init(path)
1540
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001541 with repo.config_writer() as config_writer:
1542 config_writer.set_value('user', 'email', 'user@example.com')
1543 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001544
Clark Boylanb640e052014-04-03 16:41:46 -07001545 repo.index.commit('initial commit')
1546 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001547
James E. Blair97d902e2014-08-21 13:25:56 -07001548 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001549 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001550 repo.git.clean('-x', '-f', '-d')
1551
James E. Blair97d902e2014-08-21 13:25:56 -07001552 def create_branch(self, project, branch):
1553 path = os.path.join(self.upstream_root, project)
1554 repo = git.Repo.init(path)
1555 fn = os.path.join(path, 'README')
1556
1557 branch_head = repo.create_head(branch)
1558 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001559 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001560 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001561 f.close()
1562 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001563 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001564
James E. Blair97d902e2014-08-21 13:25:56 -07001565 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001566 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001567 repo.git.clean('-x', '-f', '-d')
1568
Sachi King9f16d522016-03-16 12:20:45 +11001569 def create_commit(self, project):
1570 path = os.path.join(self.upstream_root, project)
1571 repo = git.Repo(path)
1572 repo.head.reference = repo.heads['master']
1573 file_name = os.path.join(path, 'README')
1574 with open(file_name, 'a') as f:
1575 f.write('creating fake commit\n')
1576 repo.index.add([file_name])
1577 commit = repo.index.commit('Creating a fake commit')
1578 return commit.hexsha
1579
James E. Blairb8c16472015-05-05 14:55:26 -07001580 def orderedRelease(self):
1581 # Run one build at a time to ensure non-race order:
1582 while len(self.builds):
1583 self.release(self.builds[0])
1584 self.waitUntilSettled()
1585
Clark Boylanb640e052014-04-03 16:41:46 -07001586 def release(self, job):
1587 if isinstance(job, FakeBuild):
1588 job.release()
1589 else:
1590 job.waiting = False
1591 self.log.debug("Queued job %s released" % job.unique)
1592 self.gearman_server.wakeConnections()
1593
1594 def getParameter(self, job, name):
1595 if isinstance(job, FakeBuild):
1596 return job.parameters[name]
1597 else:
1598 parameters = json.loads(job.arguments)
1599 return parameters[name]
1600
Clark Boylanb640e052014-04-03 16:41:46 -07001601 def haveAllBuildsReported(self):
1602 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001603 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001604 return False
1605 # Find out if every build that the worker has completed has been
1606 # reported back to Zuul. If it hasn't then that means a Gearman
1607 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001608 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001609 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001610 if not zbuild:
1611 # It has already been reported
1612 continue
1613 # It hasn't been reported yet.
1614 return False
1615 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001616 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001617 if connection.state == 'GRAB_WAIT':
1618 return False
1619 return True
1620
1621 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001622 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001623 for build in builds:
1624 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001625 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001626 for j in conn.related_jobs.values():
1627 if j.unique == build.uuid:
1628 client_job = j
1629 break
1630 if not client_job:
1631 self.log.debug("%s is not known to the gearman client" %
1632 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001633 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001634 if not client_job.handle:
1635 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001636 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001637 server_job = self.gearman_server.jobs.get(client_job.handle)
1638 if not server_job:
1639 self.log.debug("%s is not known to the gearman server" %
1640 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001641 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001642 if not hasattr(server_job, 'waiting'):
1643 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001644 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001645 if server_job.waiting:
1646 continue
James E. Blair17302972016-08-10 16:11:42 -07001647 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001648 self.log.debug("%s has not reported start" % build)
1649 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001650 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001651 if worker_build:
1652 if worker_build.isWaiting():
1653 continue
1654 else:
1655 self.log.debug("%s is running" % worker_build)
1656 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001657 else:
James E. Blair962220f2016-08-03 11:22:38 -07001658 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001659 return False
1660 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001661
James E. Blairdce6cea2016-12-20 16:45:32 -08001662 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001663 if self.fake_nodepool.paused:
1664 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001665 if self.sched.nodepool.requests:
1666 return False
1667 return True
1668
Jan Hruban6b71aff2015-10-22 16:58:08 +02001669 def eventQueuesEmpty(self):
1670 for queue in self.event_queues:
1671 yield queue.empty()
1672
1673 def eventQueuesJoin(self):
1674 for queue in self.event_queues:
1675 queue.join()
1676
Clark Boylanb640e052014-04-03 16:41:46 -07001677 def waitUntilSettled(self):
1678 self.log.debug("Waiting until settled...")
1679 start = time.time()
1680 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001681 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001682 self.log.error("Timeout waiting for Zuul to settle")
1683 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001684 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001685 self.log.error(" %s: %s" % (queue, queue.empty()))
1686 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001687 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001688 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001689 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001690 self.log.error("All requests completed: %s" %
1691 (self.areAllNodeRequestsComplete(),))
1692 self.log.error("Merge client jobs: %s" %
1693 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001694 raise Exception("Timeout waiting for Zuul to settle")
1695 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001696
James E. Blaire1767bc2016-08-02 10:00:27 -07001697 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001698 # have all build states propogated to zuul?
1699 if self.haveAllBuildsReported():
1700 # Join ensures that the queue is empty _and_ events have been
1701 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001702 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001703 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001704 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001705 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001706 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001707 self.areAllNodeRequestsComplete() and
1708 all(self.eventQueuesEmpty())):
1709 # The queue empty check is placed at the end to
1710 # ensure that if a component adds an event between
1711 # when locked the run handler and checked that the
1712 # components were stable, we don't erroneously
1713 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001714 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001715 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001716 self.log.debug("...settled.")
1717 return
1718 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001719 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001720 self.sched.wake_event.wait(0.1)
1721
1722 def countJobResults(self, jobs, result):
1723 jobs = filter(lambda x: x.result == result, jobs)
1724 return len(jobs)
1725
James E. Blair96c6bf82016-01-15 16:20:40 -08001726 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001727 for job in self.history:
1728 if (job.name == name and
1729 (project is None or
1730 job.parameters['ZUUL_PROJECT'] == project)):
1731 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001732 raise Exception("Unable to find job %s in history" % name)
1733
1734 def assertEmptyQueues(self):
1735 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001736 for tenant in self.sched.abide.tenants.values():
1737 for pipeline in tenant.layout.pipelines.values():
1738 for queue in pipeline.queues:
1739 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001740 print('pipeline %s queue %s contents %s' % (
1741 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001742 self.assertEqual(len(queue.queue), 0,
1743 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001744
1745 def assertReportedStat(self, key, value=None, kind=None):
1746 start = time.time()
1747 while time.time() < (start + 5):
1748 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001749 k, v = stat.split(':')
1750 if key == k:
1751 if value is None and kind is None:
1752 return
1753 elif value:
1754 if value == v:
1755 return
1756 elif kind:
1757 if v.endswith('|' + kind):
1758 return
1759 time.sleep(0.1)
1760
Clark Boylanb640e052014-04-03 16:41:46 -07001761 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001762
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001763 def assertBuilds(self, builds):
1764 """Assert that the running builds are as described.
1765
1766 The list of running builds is examined and must match exactly
1767 the list of builds described by the input.
1768
1769 :arg list builds: A list of dictionaries. Each item in the
1770 list must match the corresponding build in the build
1771 history, and each element of the dictionary must match the
1772 corresponding attribute of the build.
1773
1774 """
James E. Blair3158e282016-08-19 09:34:11 -07001775 try:
1776 self.assertEqual(len(self.builds), len(builds))
1777 for i, d in enumerate(builds):
1778 for k, v in d.items():
1779 self.assertEqual(
1780 getattr(self.builds[i], k), v,
1781 "Element %i in builds does not match" % (i,))
1782 except Exception:
1783 for build in self.builds:
1784 self.log.error("Running build: %s" % build)
1785 else:
1786 self.log.error("No running builds")
1787 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001788
James E. Blairb536ecc2016-08-31 10:11:42 -07001789 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001790 """Assert that the completed builds are as described.
1791
1792 The list of completed builds is examined and must match
1793 exactly the list of builds described by the input.
1794
1795 :arg list history: A list of dictionaries. Each item in the
1796 list must match the corresponding build in the build
1797 history, and each element of the dictionary must match the
1798 corresponding attribute of the build.
1799
James E. Blairb536ecc2016-08-31 10:11:42 -07001800 :arg bool ordered: If true, the history must match the order
1801 supplied, if false, the builds are permitted to have
1802 arrived in any order.
1803
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001804 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001805 def matches(history_item, item):
1806 for k, v in item.items():
1807 if getattr(history_item, k) != v:
1808 return False
1809 return True
James E. Blair3158e282016-08-19 09:34:11 -07001810 try:
1811 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001812 if ordered:
1813 for i, d in enumerate(history):
1814 if not matches(self.history[i], d):
1815 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001816 "Element %i in history does not match %s" %
1817 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001818 else:
1819 unseen = self.history[:]
1820 for i, d in enumerate(history):
1821 found = False
1822 for unseen_item in unseen:
1823 if matches(unseen_item, d):
1824 found = True
1825 unseen.remove(unseen_item)
1826 break
1827 if not found:
1828 raise Exception("No match found for element %i "
1829 "in history" % (i,))
1830 if unseen:
1831 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001832 except Exception:
1833 for build in self.history:
1834 self.log.error("Completed build: %s" % build)
1835 else:
1836 self.log.error("No completed builds")
1837 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001838
James E. Blair6ac368c2016-12-22 18:07:20 -08001839 def printHistory(self):
1840 """Log the build history.
1841
1842 This can be useful during tests to summarize what jobs have
1843 completed.
1844
1845 """
1846 self.log.debug("Build history:")
1847 for build in self.history:
1848 self.log.debug(build)
1849
James E. Blair59fdbac2015-12-07 17:08:06 -08001850 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001851 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1852
1853 def updateConfigLayout(self, path):
1854 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001855 if not os.path.exists(root):
1856 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001857 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1858 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05001859- tenant:
1860 name: openstack
1861 source:
1862 gerrit:
1863 config-repos:
1864 - %s
1865 """ % path)
James E. Blairf84026c2015-12-08 16:11:46 -08001866 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05001867 self.config.set('zuul', 'tenant_config',
1868 os.path.join(FIXTURE_DIR, f.name))
James E. Blair14abdf42015-12-09 16:11:53 -08001869
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001870 def addCommitToRepo(self, project, message, files,
1871 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001872 path = os.path.join(self.upstream_root, project)
1873 repo = git.Repo(path)
1874 repo.head.reference = branch
1875 zuul.merger.merger.reset_repo_to_head(repo)
1876 for fn, content in files.items():
1877 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08001878 try:
1879 os.makedirs(os.path.dirname(fn))
1880 except OSError:
1881 pass
James E. Blair14abdf42015-12-09 16:11:53 -08001882 with open(fn, 'w') as f:
1883 f.write(content)
1884 repo.index.add([fn])
1885 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08001886 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08001887 repo.heads[branch].commit = commit
1888 repo.head.reference = branch
1889 repo.git.clean('-x', '-f', '-d')
1890 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001891 if tag:
1892 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08001893 return before
1894
1895 def commitLayoutUpdate(self, orig_name, source_name):
1896 source_path = os.path.join(self.test_root, 'upstream',
1897 source_name, 'zuul.yaml')
1898 with open(source_path, 'r') as nt:
1899 before = self.addCommitToRepo(
1900 orig_name, 'Pulling content from %s' % source_name,
1901 {'zuul.yaml': nt.read()})
1902 return before
James E. Blair3f876d52016-07-22 13:07:14 -07001903
James E. Blair7fc8daa2016-08-08 15:37:15 -07001904 def addEvent(self, connection, event):
1905 """Inject a Fake (Gerrit) event.
1906
1907 This method accepts a JSON-encoded event and simulates Zuul
1908 having received it from Gerrit. It could (and should)
1909 eventually apply to any connection type, but is currently only
1910 used with Gerrit connections. The name of the connection is
1911 used to look up the corresponding server, and the event is
1912 simulated as having been received by all Zuul connections
1913 attached to that server. So if two Gerrit connections in Zuul
1914 are connected to the same Gerrit server, and you invoke this
1915 method specifying the name of one of them, the event will be
1916 received by both.
1917
1918 .. note::
1919
1920 "self.fake_gerrit.addEvent" calls should be migrated to
1921 this method.
1922
1923 :arg str connection: The name of the connection corresponding
1924 to the gerrit server.
1925 :arg str event: The JSON-encoded event.
1926
1927 """
1928 specified_conn = self.connections.connections[connection]
1929 for conn in self.connections.connections.values():
1930 if (isinstance(conn, specified_conn.__class__) and
1931 specified_conn.server == conn.server):
1932 conn.addEvent(event)
1933
James E. Blair3f876d52016-07-22 13:07:14 -07001934
1935class AnsibleZuulTestCase(ZuulTestCase):
1936 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001937 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11001938
Joshua Heskethd78b4482015-09-14 16:56:34 -06001939
1940class ZuulDBTestCase(ZuulTestCase):
1941 def setup_config(self, config_file='zuul-connections-same-gerrit.conf'):
1942 super(ZuulDBTestCase, self).setup_config(config_file)
1943 for section_name in self.config.sections():
1944 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1945 section_name, re.I)
1946 if not con_match:
1947 continue
1948
1949 if self.config.get(section_name, 'driver') == 'sql':
1950 f = MySQLSchemaFixture()
1951 self.useFixture(f)
1952 if (self.config.get(section_name, 'dburi') ==
1953 '$MYSQL_FIXTURE_DBURI$'):
1954 self.config.set(section_name, 'dburi', f.dburi)