blob: 369004895627a21aa48906f41ab99ffc953df254 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
Christian Berendtffba5df2014-06-07 21:30:22 +020017from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070018import gc
19import hashlib
20import json
21import logging
22import os
23import pprint
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylanb640e052014-04-03 16:41:46 -070031import socket
32import string
33import subprocess
34import swiftclient
James E. Blairf84026c2015-12-08 16:11:46 -080035import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070036import threading
37import time
Clark Boylanb640e052014-04-03 16:41:46 -070038
39import git
40import gear
41import fixtures
Clark Boylanb640e052014-04-03 16:41:46 -070042import statsd
43import testtools
Mike Heald8225f522014-11-21 09:52:33 +000044from git import GitCommandError
Clark Boylanb640e052014-04-03 16:41:46 -070045
Joshua Hesketh352264b2015-08-11 23:42:08 +100046import zuul.connection.gerrit
47import zuul.connection.smtp
Clark Boylanb640e052014-04-03 16:41:46 -070048import zuul.scheduler
49import zuul.webapp
50import zuul.rpclistener
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +100051import zuul.launcher.server
52import zuul.launcher.client
Clark Boylanb640e052014-04-03 16:41:46 -070053import zuul.lib.swift
James E. Blair83005782015-12-11 14:46:03 -080054import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070055import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070056import zuul.merger.merger
57import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070058import zuul.nodepool
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.reporter.gerrit
60import zuul.reporter.smtp
Joshua Hesketh850ccb62014-11-27 11:31:02 +110061import zuul.source.gerrit
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.trigger.gerrit
63import zuul.trigger.timer
James E. Blairc494d542014-08-06 09:23:52 -070064import zuul.trigger.zuultrigger
Clark Boylanb640e052014-04-03 16:41:46 -070065
66FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
67 'fixtures')
James E. Blair97d902e2014-08-21 13:25:56 -070068USE_TEMPDIR = True
Clark Boylanb640e052014-04-03 16:41:46 -070069
70logging.basicConfig(level=logging.DEBUG,
71 format='%(asctime)s %(name)-32s '
72 '%(levelname)-8s %(message)s')
73
74
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Clark Boylanb640e052014-04-03 16:41:46 -0700100class ChangeReference(git.Reference):
101 _common_path_default = "refs/changes"
102 _points_to_commits_only = True
103
104
105class FakeChange(object):
106 categories = {'APRV': ('Approved', -1, 1),
107 'CRVW': ('Code-Review', -2, 2),
108 'VRFY': ('Verified', -2, 2)}
109
110 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700111 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700112 self.gerrit = gerrit
113 self.reported = 0
114 self.queried = 0
115 self.patchsets = []
116 self.number = number
117 self.project = project
118 self.branch = branch
119 self.subject = subject
120 self.latest_patchset = 0
121 self.depends_on_change = None
122 self.needed_by_changes = []
123 self.fail_merge = False
124 self.messages = []
125 self.data = {
126 'branch': branch,
127 'comments': [],
128 'commitMessage': subject,
129 'createdOn': time.time(),
130 'id': 'I' + random_sha1(),
131 'lastUpdated': time.time(),
132 'number': str(number),
133 'open': status == 'NEW',
134 'owner': {'email': 'user@example.com',
135 'name': 'User Name',
136 'username': 'username'},
137 'patchSets': self.patchsets,
138 'project': project,
139 'status': status,
140 'subject': subject,
141 'submitRecords': [],
142 'url': 'https://hostname/%s' % number}
143
144 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700145 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.data['submitRecords'] = self.getSubmitRecords()
147 self.open = status == 'NEW'
148
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700149 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700150 path = os.path.join(self.upstream_root, self.project)
151 repo = git.Repo(path)
152 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
153 self.latest_patchset),
154 'refs/tags/init')
155 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700156 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700157 repo.git.clean('-x', '-f', '-d')
158
159 path = os.path.join(self.upstream_root, self.project)
160 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700161 for fn, content in files.items():
162 fn = os.path.join(path, fn)
163 with open(fn, 'w') as f:
164 f.write(content)
165 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700166 else:
167 for fni in range(100):
168 fn = os.path.join(path, str(fni))
169 f = open(fn, 'w')
170 for ci in range(4096):
171 f.write(random.choice(string.printable))
172 f.close()
173 repo.index.add([fn])
174
175 r = repo.index.commit(msg)
176 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700177 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 repo.git.clean('-x', '-f', '-d')
179 repo.heads['master'].checkout()
180 return r
181
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700182 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700183 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700184 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700185 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 data = ("test %s %s %s\n" %
187 (self.branch, self.number, self.latest_patchset))
188 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700189 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700190 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 ps_files = [{'file': '/COMMIT_MSG',
192 'type': 'ADDED'},
193 {'file': 'README',
194 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700196 ps_files.append({'file': f, 'type': 'ADDED'})
197 d = {'approvals': [],
198 'createdOn': time.time(),
199 'files': ps_files,
200 'number': str(self.latest_patchset),
201 'ref': 'refs/changes/1/%s/%s' % (self.number,
202 self.latest_patchset),
203 'revision': c.hexsha,
204 'uploader': {'email': 'user@example.com',
205 'name': 'User name',
206 'username': 'user'}}
207 self.data['currentPatchSet'] = d
208 self.patchsets.append(d)
209 self.data['submitRecords'] = self.getSubmitRecords()
210
211 def getPatchsetCreatedEvent(self, patchset):
212 event = {"type": "patchset-created",
213 "change": {"project": self.project,
214 "branch": self.branch,
215 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
216 "number": str(self.number),
217 "subject": self.subject,
218 "owner": {"name": "User Name"},
219 "url": "https://hostname/3"},
220 "patchSet": self.patchsets[patchset - 1],
221 "uploader": {"name": "User Name"}}
222 return event
223
224 def getChangeRestoredEvent(self):
225 event = {"type": "change-restored",
226 "change": {"project": self.project,
227 "branch": self.branch,
228 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
229 "number": str(self.number),
230 "subject": self.subject,
231 "owner": {"name": "User Name"},
232 "url": "https://hostname/3"},
233 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100234 "patchSet": self.patchsets[-1],
235 "reason": ""}
236 return event
237
238 def getChangeAbandonedEvent(self):
239 event = {"type": "change-abandoned",
240 "change": {"project": self.project,
241 "branch": self.branch,
242 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
243 "number": str(self.number),
244 "subject": self.subject,
245 "owner": {"name": "User Name"},
246 "url": "https://hostname/3"},
247 "abandoner": {"name": "User Name"},
248 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700249 "reason": ""}
250 return event
251
252 def getChangeCommentEvent(self, patchset):
253 event = {"type": "comment-added",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "author": {"name": "User Name"},
263 "approvals": [{"type": "Code-Review",
264 "description": "Code-Review",
265 "value": "0"}],
266 "comment": "This is a comment"}
267 return event
268
Joshua Hesketh642824b2014-07-01 17:54:59 +1000269 def addApproval(self, category, value, username='reviewer_john',
270 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700271 if not granted_on:
272 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000273 approval = {
274 'description': self.categories[category][0],
275 'type': category,
276 'value': str(value),
277 'by': {
278 'username': username,
279 'email': username + '@example.com',
280 },
281 'grantedOn': int(granted_on)
282 }
Clark Boylanb640e052014-04-03 16:41:46 -0700283 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
284 if x['by']['username'] == username and x['type'] == category:
285 del self.patchsets[-1]['approvals'][i]
286 self.patchsets[-1]['approvals'].append(approval)
287 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000288 'author': {'email': 'author@example.com',
289 'name': 'Patchset Author',
290 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700291 'change': {'branch': self.branch,
292 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
293 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000294 'owner': {'email': 'owner@example.com',
295 'name': 'Change Owner',
296 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700297 'project': self.project,
298 'subject': self.subject,
299 'topic': 'master',
300 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000301 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700302 'patchSet': self.patchsets[-1],
303 'type': 'comment-added'}
304 self.data['submitRecords'] = self.getSubmitRecords()
305 return json.loads(json.dumps(event))
306
307 def getSubmitRecords(self):
308 status = {}
309 for cat in self.categories.keys():
310 status[cat] = 0
311
312 for a in self.patchsets[-1]['approvals']:
313 cur = status[a['type']]
314 cat_min, cat_max = self.categories[a['type']][1:]
315 new = int(a['value'])
316 if new == cat_min:
317 cur = new
318 elif abs(new) > abs(cur):
319 cur = new
320 status[a['type']] = cur
321
322 labels = []
323 ok = True
324 for typ, cat in self.categories.items():
325 cur = status[typ]
326 cat_min, cat_max = cat[1:]
327 if cur == cat_min:
328 value = 'REJECT'
329 ok = False
330 elif cur == cat_max:
331 value = 'OK'
332 else:
333 value = 'NEED'
334 ok = False
335 labels.append({'label': cat[0], 'status': value})
336 if ok:
337 return [{'status': 'OK'}]
338 return [{'status': 'NOT_READY',
339 'labels': labels}]
340
341 def setDependsOn(self, other, patchset):
342 self.depends_on_change = other
343 d = {'id': other.data['id'],
344 'number': other.data['number'],
345 'ref': other.patchsets[patchset - 1]['ref']
346 }
347 self.data['dependsOn'] = [d]
348
349 other.needed_by_changes.append(self)
350 needed = other.data.get('neededBy', [])
351 d = {'id': self.data['id'],
352 'number': self.data['number'],
353 'ref': self.patchsets[patchset - 1]['ref'],
354 'revision': self.patchsets[patchset - 1]['revision']
355 }
356 needed.append(d)
357 other.data['neededBy'] = needed
358
359 def query(self):
360 self.queried += 1
361 d = self.data.get('dependsOn')
362 if d:
363 d = d[0]
364 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
365 d['isCurrentPatchSet'] = True
366 else:
367 d['isCurrentPatchSet'] = False
368 return json.loads(json.dumps(self.data))
369
370 def setMerged(self):
371 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000372 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700373 return
374 if self.fail_merge:
375 return
376 self.data['status'] = 'MERGED'
377 self.open = False
378
379 path = os.path.join(self.upstream_root, self.project)
380 repo = git.Repo(path)
381 repo.heads[self.branch].commit = \
382 repo.commit(self.patchsets[-1]['revision'])
383
384 def setReported(self):
385 self.reported += 1
386
387
Joshua Hesketh352264b2015-08-11 23:42:08 +1000388class FakeGerritConnection(zuul.connection.gerrit.GerritConnection):
389 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700390
Joshua Hesketh352264b2015-08-11 23:42:08 +1000391 def __init__(self, connection_name, connection_config,
Jan Hruban6b71aff2015-10-22 16:58:08 +0200392 changes_db=None, queues_db=None, upstream_root=None):
Joshua Hesketh352264b2015-08-11 23:42:08 +1000393 super(FakeGerritConnection, self).__init__(connection_name,
394 connection_config)
395
396 self.event_queue = queues_db
Clark Boylanb640e052014-04-03 16:41:46 -0700397 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
398 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000399 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700400 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200401 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700402
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700403 def addFakeChange(self, project, branch, subject, status='NEW',
404 files=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700405 self.change_number += 1
406 c = FakeChange(self, self.change_number, project, branch, subject,
407 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700408 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700409 self.changes[self.change_number] = c
410 return c
411
Clark Boylanb640e052014-04-03 16:41:46 -0700412 def review(self, project, changeid, message, action):
413 number, ps = changeid.split(',')
414 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000415
416 # Add the approval back onto the change (ie simulate what gerrit would
417 # do).
418 # Usually when zuul leaves a review it'll create a feedback loop where
419 # zuul's review enters another gerrit event (which is then picked up by
420 # zuul). However, we can't mimic this behaviour (by adding this
421 # approval event into the queue) as it stops jobs from checking what
422 # happens before this event is triggered. If a job needs to see what
423 # happens they can add their own verified event into the queue.
424 # Nevertheless, we can update change with the new review in gerrit.
425
426 for cat in ['CRVW', 'VRFY', 'APRV']:
427 if cat in action:
Joshua Hesketh352264b2015-08-11 23:42:08 +1000428 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000429
430 if 'label' in action:
431 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000432 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000433
Clark Boylanb640e052014-04-03 16:41:46 -0700434 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000435
Clark Boylanb640e052014-04-03 16:41:46 -0700436 if 'submit' in action:
437 change.setMerged()
438 if message:
439 change.setReported()
440
441 def query(self, number):
442 change = self.changes.get(int(number))
443 if change:
444 return change.query()
445 return {}
446
James E. Blairc494d542014-08-06 09:23:52 -0700447 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700448 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700449 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800450 if query.startswith('change:'):
451 # Query a specific changeid
452 changeid = query[len('change:'):]
453 l = [change.query() for change in self.changes.values()
454 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700455 elif query.startswith('message:'):
456 # Query the content of a commit message
457 msg = query[len('message:'):].strip()
458 l = [change.query() for change in self.changes.values()
459 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800460 else:
461 # Query all open changes
462 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700463 return l
James E. Blairc494d542014-08-06 09:23:52 -0700464
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700466 pass
467
Joshua Hesketh352264b2015-08-11 23:42:08 +1000468 def getGitUrl(self, project):
469 return os.path.join(self.upstream_root, project.name)
470
Clark Boylanb640e052014-04-03 16:41:46 -0700471
472class BuildHistory(object):
473 def __init__(self, **kw):
474 self.__dict__.update(kw)
475
476 def __repr__(self):
477 return ("<Completed build, result: %s name: %s #%s changes: %s>" %
478 (self.result, self.name, self.number, self.changes))
479
480
481class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200482 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700483 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700484 self.url = url
485
486 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700487 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700488 path = res.path
489 project = '/'.join(path.split('/')[2:-2])
490 ret = '001e# service=git-upload-pack\n'
491 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
492 'multi_ack thin-pack side-band side-band-64k ofs-delta '
493 'shallow no-progress include-tag multi_ack_detailed no-done\n')
494 path = os.path.join(self.upstream_root, project)
495 repo = git.Repo(path)
496 for ref in repo.refs:
497 r = ref.object.hexsha + ' ' + ref.path + '\n'
498 ret += '%04x%s' % (len(r) + 4, r)
499 ret += '0000'
500 return ret
501
502
Clark Boylanb640e052014-04-03 16:41:46 -0700503class FakeStatsd(threading.Thread):
504 def __init__(self):
505 threading.Thread.__init__(self)
506 self.daemon = True
507 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
508 self.sock.bind(('', 0))
509 self.port = self.sock.getsockname()[1]
510 self.wake_read, self.wake_write = os.pipe()
511 self.stats = []
512
513 def run(self):
514 while True:
515 poll = select.poll()
516 poll.register(self.sock, select.POLLIN)
517 poll.register(self.wake_read, select.POLLIN)
518 ret = poll.poll()
519 for (fd, event) in ret:
520 if fd == self.sock.fileno():
521 data = self.sock.recvfrom(1024)
522 if not data:
523 return
524 self.stats.append(data[0])
525 if fd == self.wake_read:
526 return
527
528 def stop(self):
529 os.write(self.wake_write, '1\n')
530
531
James E. Blaire1767bc2016-08-02 10:00:27 -0700532class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700533 log = logging.getLogger("zuul.test")
534
James E. Blairab7132b2016-08-05 12:36:22 -0700535 def __init__(self, launch_server, job, number, node):
Clark Boylanb640e052014-04-03 16:41:46 -0700536 self.daemon = True
James E. Blaire1767bc2016-08-02 10:00:27 -0700537 self.launch_server = launch_server
Clark Boylanb640e052014-04-03 16:41:46 -0700538 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700539 self.jobdir = None
Clark Boylanb640e052014-04-03 16:41:46 -0700540 self.number = number
541 self.node = node
542 self.parameters = json.loads(job.arguments)
543 self.unique = self.parameters['ZUUL_UUID']
James E. Blair3f876d52016-07-22 13:07:14 -0700544 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700545 self.wait_condition = threading.Condition()
546 self.waiting = False
547 self.aborted = False
548 self.created = time.time()
Clark Boylanb640e052014-04-03 16:41:46 -0700549 self.run_error = False
James E. Blaire1767bc2016-08-02 10:00:27 -0700550 self.changes = None
551 if 'ZUUL_CHANGE_IDS' in self.parameters:
552 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700553
554 def release(self):
555 self.wait_condition.acquire()
556 self.wait_condition.notify()
557 self.waiting = False
558 self.log.debug("Build %s released" % self.unique)
559 self.wait_condition.release()
560
561 def isWaiting(self):
562 self.wait_condition.acquire()
563 if self.waiting:
564 ret = True
565 else:
566 ret = False
567 self.wait_condition.release()
568 return ret
569
570 def _wait(self):
571 self.wait_condition.acquire()
572 self.waiting = True
573 self.log.debug("Build %s waiting" % self.unique)
574 self.wait_condition.wait()
575 self.wait_condition.release()
576
577 def run(self):
578 data = {
579 'url': 'https://server/job/%s/%s/' % (self.name, self.number),
580 'name': self.name,
581 'number': self.number,
James E. Blaire1767bc2016-08-02 10:00:27 -0700582 'manager': self.launch_server.worker.worker_id,
Clark Boylanb640e052014-04-03 16:41:46 -0700583 'worker_name': 'My Worker',
584 'worker_hostname': 'localhost',
585 'worker_ips': ['127.0.0.1', '192.168.1.1'],
586 'worker_fqdn': 'zuul.example.org',
587 'worker_program': 'FakeBuilder',
588 'worker_version': 'v1.1',
589 'worker_extra': {'something': 'else'}
590 }
591
592 self.log.debug('Running build %s' % self.unique)
593
594 self.job.sendWorkData(json.dumps(data))
595 self.log.debug('Sent WorkData packet with %s' % json.dumps(data))
596 self.job.sendWorkStatus(0, 100)
597
James E. Blaire1767bc2016-08-02 10:00:27 -0700598 if self.launch_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700599 self.log.debug('Holding build %s' % self.unique)
600 self._wait()
601 self.log.debug("Build %s continuing" % self.unique)
602
Clark Boylanb640e052014-04-03 16:41:46 -0700603 result = 'SUCCESS'
604 if (('ZUUL_REF' in self.parameters) and
James E. Blaire1767bc2016-08-02 10:00:27 -0700605 self.launch_server.shouldFailTest(self.name,
606 self.parameters['ZUUL_REF'])):
Clark Boylanb640e052014-04-03 16:41:46 -0700607 result = 'FAILURE'
608 if self.aborted:
609 result = 'ABORTED'
610
611 if self.run_error:
Clark Boylanb640e052014-04-03 16:41:46 -0700612 result = 'RUN_ERROR'
Clark Boylanb640e052014-04-03 16:41:46 -0700613
James E. Blaire1767bc2016-08-02 10:00:27 -0700614 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700615
James E. Blair962220f2016-08-03 11:22:38 -0700616 def hasChanges(self, *commits):
617 project = self.parameters['ZUUL_PROJECT']
618 path = os.path.join(self.jobdir.git_root, project)
619 repo = git.Repo(path)
620 ref = self.parameters['ZUUL_REF']
621 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
622 commit_messages = ['%s-1' % commit.subject for commit in commits]
623 self.log.debug("Checking if build %s has changes; commit_messages %s;"
624 " repo_messages %s" % (self, commit_messages,
625 repo_messages))
626 for msg in commit_messages:
627 if msg not in repo_messages:
628 self.log.debug(" messages do not match")
629 return False
630 self.log.debug(" OK")
631 return True
632
Clark Boylanb640e052014-04-03 16:41:46 -0700633
Joshua Hesketh0c54b2a2016-04-11 21:23:33 +1000634class RecordingLaunchServer(zuul.launcher.server.LaunchServer):
James E. Blairf5dbd002015-12-23 15:26:17 -0800635 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700636 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blairf5dbd002015-12-23 15:26:17 -0800637 super(RecordingLaunchServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700638 self.hold_jobs_in_build = False
639 self.lock = threading.Lock()
640 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700641 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700642 self._build_counter_lock = threading.Lock()
643 self.build_counter = 0
644 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700645 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800646
James E. Blaire1767bc2016-08-02 10:00:27 -0700647 def addFailTest(self, name, change):
648 l = self.fail_tests.get(name, [])
649 l.append(change)
650 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800651
James E. Blaire1767bc2016-08-02 10:00:27 -0700652 def shouldFailTest(self, name, ref):
653 l = self.fail_tests.get(name, [])
654 for change in l:
655 if self.test.ref_has_change(ref, change):
656 return True
657 return False
James E. Blairf5dbd002015-12-23 15:26:17 -0800658
James E. Blair962220f2016-08-03 11:22:38 -0700659 def release(self, regex=None):
660 builds = self.running_builds[:]
661 self.log.debug("Releasing build %s (%s)" % (regex,
662 len(self.running_builds)))
663 for build in builds:
664 if not regex or re.match(regex, build.name):
665 self.log.debug("Releasing build %s" %
666 (build.parameters['ZUUL_UUID']))
667 build.release()
668 else:
669 self.log.debug("Not releasing build %s" %
670 (build.parameters['ZUUL_UUID']))
671 self.log.debug("Done releasing builds %s (%s)" %
672 (regex, len(self.running_builds)))
673
James E. Blairab7132b2016-08-05 12:36:22 -0700674 def launch(self, job):
James E. Blaire1767bc2016-08-02 10:00:27 -0700675 with self._build_counter_lock:
676 self.build_counter += 1
677 build_counter = self.build_counter
678 node = None
James E. Blairab7132b2016-08-05 12:36:22 -0700679 build = FakeBuild(self, job, build_counter, node)
James E. Blaire1767bc2016-08-02 10:00:27 -0700680 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700681 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700682 self.job_builds[job.unique] = build
683 super(RecordingLaunchServer, self).launch(job)
684
685 def runAnsible(self, jobdir, job):
686 build = self.job_builds[job.unique]
687 build.jobdir = jobdir
James E. Blaire1767bc2016-08-02 10:00:27 -0700688
689 if self._run_ansible:
690 result = super(RecordingLaunchServer, self).runAnsible(jobdir, job)
691 else:
692 result = build.run()
693
694 self.lock.acquire()
695 self.build_history.append(
696 BuildHistory(name=build.name, number=build.number,
697 result=result, changes=build.changes, node=build.node,
698 uuid=build.unique, parameters=build.parameters,
699 pipeline=build.parameters['ZUUL_PIPELINE'])
700 )
James E. Blairab7132b2016-08-05 12:36:22 -0700701 self.running_builds.remove(build)
702 del self.job_builds[job.unique]
James E. Blaire1767bc2016-08-02 10:00:27 -0700703 self.lock.release()
704 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800705
706
Clark Boylanb640e052014-04-03 16:41:46 -0700707class FakeWorker(gear.Worker):
708 def __init__(self, worker_id, test):
709 super(FakeWorker, self).__init__(worker_id)
Clark Boylanb640e052014-04-03 16:41:46 -0700710 self.build_history = []
711 self.running_builds = []
712 self.build_counter = 0
713 self.fail_tests = {}
714 self.test = test
715
James E. Blair3f876d52016-07-22 13:07:14 -0700716 self.registerFunction('launcher:launch')
Clark Boylanb640e052014-04-03 16:41:46 -0700717 self.hold_jobs_in_build = False
718 self.lock = threading.Lock()
719 self.__work_thread = threading.Thread(target=self.work)
720 self.__work_thread.daemon = True
721 self.__work_thread.start()
722
723 def handleJob(self, job):
724 parts = job.name.split(":")
James E. Blair3f876d52016-07-22 13:07:14 -0700725 cmd = parts[1]
726 if cmd == 'launch':
727 self.handleLaunch(job)
Clark Boylanb640e052014-04-03 16:41:46 -0700728 elif cmd == 'stop':
James E. Blair3f876d52016-07-22 13:07:14 -0700729 self.handleStop(job)
Clark Boylanb640e052014-04-03 16:41:46 -0700730
James E. Blair3f876d52016-07-22 13:07:14 -0700731 def handleLaunch(self, job):
732 # TODOv3(jeblair): handle nodes
733 node = None
Clark Boylanb640e052014-04-03 16:41:46 -0700734 build = FakeBuild(self, job, self.build_counter, node)
735 job.build = build
Clark Boylanb640e052014-04-03 16:41:46 -0700736 self.build_counter += 1
737
738 self.running_builds.append(build)
739 build.start()
740
James E. Blair3f876d52016-07-22 13:07:14 -0700741 def handleStop(self, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700742 self.log.debug("handle stop")
743 parameters = json.loads(job.arguments)
744 name = parameters['name']
745 number = parameters['number']
746 for build in self.running_builds:
747 if build.name == name and build.number == number:
748 build.aborted = True
749 build.release()
750 job.sendWorkComplete()
751 return
752 job.sendWorkFail()
753
Clark Boylanb640e052014-04-03 16:41:46 -0700754 def work(self):
755 while self.running:
756 try:
757 job = self.getJob()
758 except gear.InterruptedError:
759 continue
760 try:
761 self.handleJob(job)
762 except:
763 self.log.exception("Worker exception:")
764
765 def addFailTest(self, name, change):
766 l = self.fail_tests.get(name, [])
767 l.append(change)
768 self.fail_tests[name] = l
769
770 def shouldFailTest(self, name, ref):
771 l = self.fail_tests.get(name, [])
772 for change in l:
773 if self.test.ref_has_change(ref, change):
774 return True
775 return False
776
Clark Boylanb640e052014-04-03 16:41:46 -0700777
778class FakeGearmanServer(gear.Server):
779 def __init__(self):
780 self.hold_jobs_in_queue = False
781 super(FakeGearmanServer, self).__init__(0)
782
783 def getJobForConnection(self, connection, peek=False):
784 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
785 for job in queue:
786 if not hasattr(job, 'waiting'):
787 if job.name.startswith('build:'):
788 job.waiting = self.hold_jobs_in_queue
789 else:
790 job.waiting = False
791 if job.waiting:
792 continue
793 if job.name in connection.functions:
794 if not peek:
795 queue.remove(job)
796 connection.related_jobs[job.handle] = job
797 job.worker_connection = connection
798 job.running = True
799 return job
800 return None
801
802 def release(self, regex=None):
803 released = False
804 qlen = (len(self.high_queue) + len(self.normal_queue) +
805 len(self.low_queue))
806 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
807 for job in self.getQueue():
808 cmd, name = job.name.split(':')
809 if cmd != 'build':
810 continue
811 if not regex or re.match(regex, name):
812 self.log.debug("releasing queued job %s" %
813 job.unique)
814 job.waiting = False
815 released = True
816 else:
817 self.log.debug("not releasing queued job %s" %
818 job.unique)
819 if released:
820 self.wakeConnections()
821 qlen = (len(self.high_queue) + len(self.normal_queue) +
822 len(self.low_queue))
823 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
824
825
826class FakeSMTP(object):
827 log = logging.getLogger('zuul.FakeSMTP')
828
829 def __init__(self, messages, server, port):
830 self.server = server
831 self.port = port
832 self.messages = messages
833
834 def sendmail(self, from_email, to_email, msg):
835 self.log.info("Sending email from %s, to %s, with msg %s" % (
836 from_email, to_email, msg))
837
838 headers = msg.split('\n\n', 1)[0]
839 body = msg.split('\n\n', 1)[1]
840
841 self.messages.append(dict(
842 from_email=from_email,
843 to_email=to_email,
844 msg=msg,
845 headers=headers,
846 body=body,
847 ))
848
849 return True
850
851 def quit(self):
852 return True
853
854
855class FakeSwiftClientConnection(swiftclient.client.Connection):
856 def post_account(self, headers):
857 # Do nothing
858 pass
859
860 def get_auth(self):
861 # Returns endpoint and (unused) auth token
862 endpoint = os.path.join('https://storage.example.org', 'V1',
863 'AUTH_account')
864 return endpoint, ''
865
866
Maru Newby3fe5f852015-01-13 04:22:14 +0000867class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -0700868 log = logging.getLogger("zuul.test")
869
870 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +0000871 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -0700872 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
873 try:
874 test_timeout = int(test_timeout)
875 except ValueError:
876 # If timeout value is invalid do not set a timeout.
877 test_timeout = 0
878 if test_timeout > 0:
879 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
880
881 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
882 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
883 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
884 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
885 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
886 os.environ.get('OS_STDERR_CAPTURE') == '1'):
887 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
888 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
889 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
890 os.environ.get('OS_LOG_CAPTURE') == '1'):
891 self.useFixture(fixtures.FakeLogger(
892 level=logging.DEBUG,
893 format='%(asctime)s %(name)-32s '
894 '%(levelname)-8s %(message)s'))
Maru Newby3fe5f852015-01-13 04:22:14 +0000895
Morgan Fainbergd34e0b42016-06-09 19:10:38 -0700896 # NOTE(notmorgan): Extract logging overrides for specific libraries
897 # from the OS_LOG_DEFAULTS env and create FakeLogger fixtures for
898 # each. This is used to limit the output during test runs from
899 # libraries that zuul depends on such as gear.
900 log_defaults_from_env = os.environ.get('OS_LOG_DEFAULTS')
901
902 if log_defaults_from_env:
903 for default in log_defaults_from_env.split(','):
904 try:
905 name, level_str = default.split('=', 1)
906 level = getattr(logging, level_str, logging.DEBUG)
907 self.useFixture(fixtures.FakeLogger(
908 name=name,
909 level=level,
910 format='%(asctime)s %(name)-32s '
911 '%(levelname)-8s %(message)s'))
912 except ValueError:
913 # NOTE(notmorgan): Invalid format of the log default,
914 # skip and don't try and apply a logger for the
915 # specified module
916 pass
917
Maru Newby3fe5f852015-01-13 04:22:14 +0000918
919class ZuulTestCase(BaseTestCase):
James E. Blair83005782015-12-11 14:46:03 -0800920 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -0700921 run_ansible = False
James E. Blair3f876d52016-07-22 13:07:14 -0700922
923 def _startMerger(self):
924 self.merge_server = zuul.merger.server.MergeServer(self.config,
925 self.connections)
926 self.merge_server.start()
927
Maru Newby3fe5f852015-01-13 04:22:14 +0000928 def setUp(self):
929 super(ZuulTestCase, self).setUp()
James E. Blair97d902e2014-08-21 13:25:56 -0700930 if USE_TEMPDIR:
931 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000932 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
933 ).path
James E. Blair97d902e2014-08-21 13:25:56 -0700934 else:
935 tmp_root = os.environ.get("ZUUL_TEST_ROOT")
Clark Boylanb640e052014-04-03 16:41:46 -0700936 self.test_root = os.path.join(tmp_root, "zuul-test")
937 self.upstream_root = os.path.join(self.test_root, "upstream")
938 self.git_root = os.path.join(self.test_root, "git")
James E. Blairce8a2132016-05-19 15:21:52 -0700939 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -0700940
941 if os.path.exists(self.test_root):
942 shutil.rmtree(self.test_root)
943 os.makedirs(self.test_root)
944 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700945 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700946
947 # Make per test copy of Configuration.
948 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -0800949 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +1100950 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -0800951 self.config.get('zuul', 'tenant_config')))
Clark Boylanb640e052014-04-03 16:41:46 -0700952 self.config.set('merger', 'git_dir', self.git_root)
James E. Blairce8a2132016-05-19 15:21:52 -0700953 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -0700954
955 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700956 # TODOv3(jeblair): remove these and replace with new git
957 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -0700958 self.init_repo("org/project3")
James E. Blair97d902e2014-08-21 13:25:56 -0700959 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -0700960 self.init_repo("org/project5")
961 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -0700962 self.init_repo("org/one-job-project")
963 self.init_repo("org/nonvoting-project")
964 self.init_repo("org/templated-project")
965 self.init_repo("org/layered-project")
966 self.init_repo("org/node-project")
967 self.init_repo("org/conflict-project")
968 self.init_repo("org/noop-project")
969 self.init_repo("org/experimental-project")
Evgeny Antyshevd6e546c2015-06-11 15:13:57 +0000970 self.init_repo("org/no-jobs-project")
Clark Boylanb640e052014-04-03 16:41:46 -0700971
James E. Blair83005782015-12-11 14:46:03 -0800972 self.setup_repos()
973
Clark Boylanb640e052014-04-03 16:41:46 -0700974 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +1000975 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
976 # see: https://github.com/jsocol/pystatsd/issues/61
977 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -0700978 os.environ['STATSD_PORT'] = str(self.statsd.port)
979 self.statsd.start()
980 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +0300981 reload_module(statsd)
982 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -0700983
984 self.gearman_server = FakeGearmanServer()
985
986 self.config.set('gearman', 'port', str(self.gearman_server.port))
987
Joshua Hesketh352264b2015-08-11 23:42:08 +1000988 zuul.source.gerrit.GerritSource.replication_timeout = 1.5
989 zuul.source.gerrit.GerritSource.replication_retry_interval = 0.5
990 zuul.connection.gerrit.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -0700991
Joshua Hesketh352264b2015-08-11 23:42:08 +1000992 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -0700993
994 self.useFixture(fixtures.MonkeyPatch('swiftclient.client.Connection',
995 FakeSwiftClientConnection))
996 self.swift = zuul.lib.swift.Swift(self.config)
997
Jan Hruban6b71aff2015-10-22 16:58:08 +0200998 self.event_queues = [
999 self.sched.result_event_queue,
1000 self.sched.trigger_event_queue
1001 ]
1002
James E. Blairfef78942016-03-11 16:28:56 -08001003 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001004 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001005
Clark Boylanb640e052014-04-03 16:41:46 -07001006 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001007 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001008 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001009 return FakeURLOpener(self.upstream_root, *args, **kw)
1010
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001011 old_urlopen = urllib.request.urlopen
1012 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001013
James E. Blair3f876d52016-07-22 13:07:14 -07001014 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001015
James E. Blaire1767bc2016-08-02 10:00:27 -07001016 self.launch_server = RecordingLaunchServer(
1017 self.config, self.connections, _run_ansible=self.run_ansible)
1018 self.launch_server.start()
1019 self.history = self.launch_server.build_history
1020 self.builds = self.launch_server.running_builds
1021
1022 self.launch_client = zuul.launcher.client.LaunchClient(
James E. Blair82938472016-01-11 14:38:13 -08001023 self.config, self.sched, self.swift)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001024 self.merge_client = zuul.merger.client.MergeClient(
1025 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001026 self.nodepool = zuul.nodepool.Nodepool(self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001027
James E. Blaire1767bc2016-08-02 10:00:27 -07001028 self.sched.setLauncher(self.launch_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001029 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001030 self.sched.setNodepool(self.nodepool)
Clark Boylanb640e052014-04-03 16:41:46 -07001031
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001032 self.webapp = zuul.webapp.WebApp(
1033 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001034 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001035
1036 self.sched.start()
1037 self.sched.reconfigure(self.config)
1038 self.sched.resume()
1039 self.webapp.start()
1040 self.rpc.start()
James E. Blaire1767bc2016-08-02 10:00:27 -07001041 self.launch_client.gearman.waitForServer()
Clark Boylanb640e052014-04-03 16:41:46 -07001042
1043 self.addCleanup(self.assertFinalState)
1044 self.addCleanup(self.shutdown)
1045
James E. Blairfef78942016-03-11 16:28:56 -08001046 def configure_connections(self):
Joshua Hesketh352264b2015-08-11 23:42:08 +10001047 # Register connections from the config
1048 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001049
Joshua Hesketh352264b2015-08-11 23:42:08 +10001050 def FakeSMTPFactory(*args, **kw):
1051 args = [self.smtp_messages] + list(args)
1052 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001053
Joshua Hesketh352264b2015-08-11 23:42:08 +10001054 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001055
Joshua Hesketh352264b2015-08-11 23:42:08 +10001056 # Set a changes database so multiple FakeGerrit's can report back to
1057 # a virtual canonical database given by the configured hostname
1058 self.gerrit_changes_dbs = {}
1059 self.gerrit_queues_dbs = {}
James E. Blairfef78942016-03-11 16:28:56 -08001060 self.connections = zuul.lib.connections.ConnectionRegistry()
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001061
Joshua Hesketh352264b2015-08-11 23:42:08 +10001062 for section_name in self.config.sections():
1063 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
1064 section_name, re.I)
1065 if not con_match:
1066 continue
1067 con_name = con_match.group(2)
1068 con_config = dict(self.config.items(section_name))
1069
1070 if 'driver' not in con_config:
1071 raise Exception("No driver specified for connection %s."
1072 % con_name)
1073
1074 con_driver = con_config['driver']
1075
1076 # TODO(jhesketh): load the required class automatically
1077 if con_driver == 'gerrit':
Joshua Heskethacccffc2015-03-31 23:38:17 +11001078 if con_config['server'] not in self.gerrit_changes_dbs.keys():
1079 self.gerrit_changes_dbs[con_config['server']] = {}
1080 if con_config['server'] not in self.gerrit_queues_dbs.keys():
1081 self.gerrit_queues_dbs[con_config['server']] = \
1082 Queue.Queue()
1083 self.event_queues.append(
1084 self.gerrit_queues_dbs[con_config['server']])
James E. Blair83005782015-12-11 14:46:03 -08001085 self.connections.connections[con_name] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001086 con_name, con_config,
Joshua Heskethacccffc2015-03-31 23:38:17 +11001087 changes_db=self.gerrit_changes_dbs[con_config['server']],
1088 queues_db=self.gerrit_queues_dbs[con_config['server']],
Jan Hruban6b71aff2015-10-22 16:58:08 +02001089 upstream_root=self.upstream_root
Joshua Hesketh352264b2015-08-11 23:42:08 +10001090 )
James E. Blair83005782015-12-11 14:46:03 -08001091 setattr(self, 'fake_' + con_name,
1092 self.connections.connections[con_name])
Joshua Hesketh352264b2015-08-11 23:42:08 +10001093 elif con_driver == 'smtp':
James E. Blair83005782015-12-11 14:46:03 -08001094 self.connections.connections[con_name] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001095 zuul.connection.smtp.SMTPConnection(con_name, con_config)
1096 else:
1097 raise Exception("Unknown driver, %s, for connection %s"
1098 % (con_config['driver'], con_name))
1099
1100 # If the [gerrit] or [smtp] sections still exist, load them in as a
1101 # connection named 'gerrit' or 'smtp' respectfully
1102
1103 if 'gerrit' in self.config.sections():
1104 self.gerrit_changes_dbs['gerrit'] = {}
1105 self.gerrit_queues_dbs['gerrit'] = Queue.Queue()
Jan Hruban6b71aff2015-10-22 16:58:08 +02001106 self.event_queues.append(self.gerrit_queues_dbs['gerrit'])
James E. Blair83005782015-12-11 14:46:03 -08001107 self.connections.connections['gerrit'] = FakeGerritConnection(
Joshua Hesketh352264b2015-08-11 23:42:08 +10001108 '_legacy_gerrit', dict(self.config.items('gerrit')),
1109 changes_db=self.gerrit_changes_dbs['gerrit'],
1110 queues_db=self.gerrit_queues_dbs['gerrit'])
1111
1112 if 'smtp' in self.config.sections():
James E. Blair83005782015-12-11 14:46:03 -08001113 self.connections.connections['smtp'] = \
Joshua Hesketh352264b2015-08-11 23:42:08 +10001114 zuul.connection.smtp.SMTPConnection(
1115 '_legacy_smtp', dict(self.config.items('smtp')))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001116
James E. Blair83005782015-12-11 14:46:03 -08001117 def setup_config(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001118 """Per test config object. Override to set different config."""
1119 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001120 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair2a629ec2015-12-22 15:32:02 -08001121 if hasattr(self, 'tenant_config_file'):
1122 self.config.set('zuul', 'tenant_config', self.tenant_config_file)
James E. Blair96c6bf82016-01-15 16:20:40 -08001123 git_path = os.path.join(
1124 os.path.dirname(
1125 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1126 'git')
1127 if os.path.exists(git_path):
1128 for reponame in os.listdir(git_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001129 project = reponame.replace('_', '/')
1130 self.copyDirToRepo(project,
James E. Blair96c6bf82016-01-15 16:20:40 -08001131 os.path.join(git_path, reponame))
1132
1133 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001134 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001135
1136 files = {}
1137 for (dirpath, dirnames, filenames) in os.walk(source_path):
1138 for filename in filenames:
1139 test_tree_filepath = os.path.join(dirpath, filename)
1140 common_path = os.path.commonprefix([test_tree_filepath,
1141 source_path])
1142 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1143 with open(test_tree_filepath, 'r') as f:
1144 content = f.read()
1145 files[relative_filepath] = content
1146 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001147 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001148
1149 def setup_repos(self):
1150 """Subclasses can override to manipulate repos before tests"""
1151 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001152
1153 def assertFinalState(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001154 # Make sure that git.Repo objects have been garbage collected.
1155 repos = []
1156 gc.collect()
1157 for obj in gc.get_objects():
1158 if isinstance(obj, git.Repo):
1159 repos.append(obj)
1160 self.assertEqual(len(repos), 0)
1161 self.assertEmptyQueues()
James E. Blair83005782015-12-11 14:46:03 -08001162 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001163 for tenant in self.sched.abide.tenants.values():
1164 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001165 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001166 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001167
1168 def shutdown(self):
1169 self.log.debug("Shutting down after tests")
James E. Blaire1767bc2016-08-02 10:00:27 -07001170 self.launch_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001171 self.merge_server.stop()
1172 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001173 self.merge_client.stop()
James E. Blaire1767bc2016-08-02 10:00:27 -07001174 self.launch_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001175 self.sched.stop()
1176 self.sched.join()
1177 self.statsd.stop()
1178 self.statsd.join()
1179 self.webapp.stop()
1180 self.webapp.join()
1181 self.rpc.stop()
1182 self.rpc.join()
1183 self.gearman_server.shutdown()
1184 threads = threading.enumerate()
1185 if len(threads) > 1:
1186 self.log.error("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001187
1188 def init_repo(self, project):
1189 parts = project.split('/')
1190 path = os.path.join(self.upstream_root, *parts[:-1])
1191 if not os.path.exists(path):
1192 os.makedirs(path)
1193 path = os.path.join(self.upstream_root, project)
1194 repo = git.Repo.init(path)
1195
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001196 with repo.config_writer() as config_writer:
1197 config_writer.set_value('user', 'email', 'user@example.com')
1198 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001199
Clark Boylanb640e052014-04-03 16:41:46 -07001200 repo.index.commit('initial commit')
1201 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001202
James E. Blair97d902e2014-08-21 13:25:56 -07001203 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001204 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001205 repo.git.clean('-x', '-f', '-d')
1206
James E. Blair97d902e2014-08-21 13:25:56 -07001207 def create_branch(self, project, branch):
1208 path = os.path.join(self.upstream_root, project)
1209 repo = git.Repo.init(path)
1210 fn = os.path.join(path, 'README')
1211
1212 branch_head = repo.create_head(branch)
1213 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001214 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001215 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001216 f.close()
1217 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001218 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001219
James E. Blair97d902e2014-08-21 13:25:56 -07001220 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001221 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001222 repo.git.clean('-x', '-f', '-d')
1223
Sachi King9f16d522016-03-16 12:20:45 +11001224 def create_commit(self, project):
1225 path = os.path.join(self.upstream_root, project)
1226 repo = git.Repo(path)
1227 repo.head.reference = repo.heads['master']
1228 file_name = os.path.join(path, 'README')
1229 with open(file_name, 'a') as f:
1230 f.write('creating fake commit\n')
1231 repo.index.add([file_name])
1232 commit = repo.index.commit('Creating a fake commit')
1233 return commit.hexsha
1234
Clark Boylanb640e052014-04-03 16:41:46 -07001235 def ref_has_change(self, ref, change):
1236 path = os.path.join(self.git_root, change.project)
1237 repo = git.Repo(path)
Mike Heald8225f522014-11-21 09:52:33 +00001238 try:
1239 for commit in repo.iter_commits(ref):
1240 if commit.message.strip() == ('%s-1' % change.subject):
1241 return True
1242 except GitCommandError:
1243 pass
Clark Boylanb640e052014-04-03 16:41:46 -07001244 return False
1245
James E. Blairb8c16472015-05-05 14:55:26 -07001246 def orderedRelease(self):
1247 # Run one build at a time to ensure non-race order:
1248 while len(self.builds):
1249 self.release(self.builds[0])
1250 self.waitUntilSettled()
1251
Clark Boylanb640e052014-04-03 16:41:46 -07001252 def release(self, job):
1253 if isinstance(job, FakeBuild):
1254 job.release()
1255 else:
1256 job.waiting = False
1257 self.log.debug("Queued job %s released" % job.unique)
1258 self.gearman_server.wakeConnections()
1259
1260 def getParameter(self, job, name):
1261 if isinstance(job, FakeBuild):
1262 return job.parameters[name]
1263 else:
1264 parameters = json.loads(job.arguments)
1265 return parameters[name]
1266
1267 def resetGearmanServer(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001268 self.launch_server.worker.setFunctions([])
Clark Boylanb640e052014-04-03 16:41:46 -07001269 while True:
1270 done = True
1271 for connection in self.gearman_server.active_connections:
1272 if (connection.functions and
1273 connection.client_id not in ['Zuul RPC Listener',
1274 'Zuul Merger']):
1275 done = False
1276 if done:
1277 break
1278 time.sleep(0)
1279 self.gearman_server.functions = set()
1280 self.rpc.register()
Clark Boylanb640e052014-04-03 16:41:46 -07001281
1282 def haveAllBuildsReported(self):
1283 # See if Zuul is waiting on a meta job to complete
James E. Blaire1767bc2016-08-02 10:00:27 -07001284 if self.launch_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001285 return False
1286 # Find out if every build that the worker has completed has been
1287 # reported back to Zuul. If it hasn't then that means a Gearman
1288 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001289 for build in self.history:
James E. Blaire1767bc2016-08-02 10:00:27 -07001290 zbuild = self.launch_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001291 if not zbuild:
1292 # It has already been reported
1293 continue
1294 # It hasn't been reported yet.
1295 return False
1296 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blaire1767bc2016-08-02 10:00:27 -07001297 for connection in self.launch_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001298 if connection.state == 'GRAB_WAIT':
1299 return False
1300 return True
1301
1302 def areAllBuildsWaiting(self):
James E. Blaire1767bc2016-08-02 10:00:27 -07001303 builds = self.launch_client.builds.values()
Clark Boylanb640e052014-04-03 16:41:46 -07001304 for build in builds:
1305 client_job = None
James E. Blaire1767bc2016-08-02 10:00:27 -07001306 for conn in self.launch_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001307 for j in conn.related_jobs.values():
1308 if j.unique == build.uuid:
1309 client_job = j
1310 break
1311 if not client_job:
1312 self.log.debug("%s is not known to the gearman client" %
1313 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001314 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001315 if not client_job.handle:
1316 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001317 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001318 server_job = self.gearman_server.jobs.get(client_job.handle)
1319 if not server_job:
1320 self.log.debug("%s is not known to the gearman server" %
1321 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001322 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001323 if not hasattr(server_job, 'waiting'):
1324 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001325 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001326 if server_job.waiting:
1327 continue
James E. Blairbbda4702016-03-09 15:19:56 -08001328 if build.number is None:
1329 self.log.debug("%s has not reported start" % build)
1330 return False
James E. Blairab7132b2016-08-05 12:36:22 -07001331 worker_build = self.launch_server.job_builds.get(server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001332 if worker_build:
1333 if worker_build.isWaiting():
1334 continue
1335 else:
1336 self.log.debug("%s is running" % worker_build)
1337 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001338 else:
James E. Blair962220f2016-08-03 11:22:38 -07001339 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001340 return False
1341 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001342
Jan Hruban6b71aff2015-10-22 16:58:08 +02001343 def eventQueuesEmpty(self):
1344 for queue in self.event_queues:
1345 yield queue.empty()
1346
1347 def eventQueuesJoin(self):
1348 for queue in self.event_queues:
1349 queue.join()
1350
Clark Boylanb640e052014-04-03 16:41:46 -07001351 def waitUntilSettled(self):
1352 self.log.debug("Waiting until settled...")
1353 start = time.time()
1354 while True:
1355 if time.time() - start > 10:
James E. Blair622c9682016-06-09 08:14:53 -07001356 self.log.debug("Queue status:")
1357 for queue in self.event_queues:
1358 self.log.debug(" %s: %s" % (queue, queue.empty()))
1359 self.log.debug("All builds waiting: %s" %
1360 (self.areAllBuildsWaiting(),))
Clark Boylanb640e052014-04-03 16:41:46 -07001361 raise Exception("Timeout waiting for Zuul to settle")
1362 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001363
James E. Blaire1767bc2016-08-02 10:00:27 -07001364 self.launch_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001365 # have all build states propogated to zuul?
1366 if self.haveAllBuildsReported():
1367 # Join ensures that the queue is empty _and_ events have been
1368 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001369 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001370 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001371 if (not self.merge_client.jobs and
Jan Hruban6b71aff2015-10-22 16:58:08 +02001372 all(self.eventQueuesEmpty()) and
Clark Boylanb640e052014-04-03 16:41:46 -07001373 self.haveAllBuildsReported() and
1374 self.areAllBuildsWaiting()):
1375 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001376 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001377 self.log.debug("...settled.")
1378 return
1379 self.sched.run_handler_lock.release()
James E. Blaire1767bc2016-08-02 10:00:27 -07001380 self.launch_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001381 self.sched.wake_event.wait(0.1)
1382
1383 def countJobResults(self, jobs, result):
1384 jobs = filter(lambda x: x.result == result, jobs)
1385 return len(jobs)
1386
James E. Blair96c6bf82016-01-15 16:20:40 -08001387 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001388 for job in self.history:
1389 if (job.name == name and
1390 (project is None or
1391 job.parameters['ZUUL_PROJECT'] == project)):
1392 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001393 raise Exception("Unable to find job %s in history" % name)
1394
1395 def assertEmptyQueues(self):
1396 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001397 for tenant in self.sched.abide.tenants.values():
1398 for pipeline in tenant.layout.pipelines.values():
1399 for queue in pipeline.queues:
1400 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001401 print('pipeline %s queue %s contents %s' % (
1402 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001403 self.assertEqual(len(queue.queue), 0,
1404 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001405
1406 def assertReportedStat(self, key, value=None, kind=None):
1407 start = time.time()
1408 while time.time() < (start + 5):
1409 for stat in self.statsd.stats:
1410 pprint.pprint(self.statsd.stats)
1411 k, v = stat.split(':')
1412 if key == k:
1413 if value is None and kind is None:
1414 return
1415 elif value:
1416 if value == v:
1417 return
1418 elif kind:
1419 if v.endswith('|' + kind):
1420 return
1421 time.sleep(0.1)
1422
1423 pprint.pprint(self.statsd.stats)
1424 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001425
1426 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001427 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1428
1429 def updateConfigLayout(self, path):
1430 root = os.path.join(self.test_root, "config")
1431 os.makedirs(root)
1432 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1433 f.write("""
1434tenants:
1435 - name: openstack
1436 include:
1437 - %s
1438 """ % os.path.abspath(path))
1439 f.close()
1440 self.config.set('zuul', 'tenant_config', f.name)
James E. Blair14abdf42015-12-09 16:11:53 -08001441
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001442 def addCommitToRepo(self, project, message, files,
1443 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08001444 path = os.path.join(self.upstream_root, project)
1445 repo = git.Repo(path)
1446 repo.head.reference = branch
1447 zuul.merger.merger.reset_repo_to_head(repo)
1448 for fn, content in files.items():
1449 fn = os.path.join(path, fn)
1450 with open(fn, 'w') as f:
1451 f.write(content)
1452 repo.index.add([fn])
1453 commit = repo.index.commit(message)
1454 repo.heads[branch].commit = commit
1455 repo.head.reference = branch
1456 repo.git.clean('-x', '-f', '-d')
1457 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001458 if tag:
1459 repo.create_tag(tag)
James E. Blair3f876d52016-07-22 13:07:14 -07001460
1461
1462class AnsibleZuulTestCase(ZuulTestCase):
1463 """ZuulTestCase but with an actual ansible launcher running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07001464 run_ansible = True