Skip to content

Commit d08d6ac

Browse files
committed
simpler more reliable and faster integration tests
1 parent 2e2f9c4 commit d08d6ac

File tree

2 files changed

+150
-59
lines changed

2 files changed

+150
-59
lines changed

requirements-dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
coverage==3.7.1
2-
pytest==2.7.2
2+
pytest==2.8.0
33
pytest-cov==2.1.0
4+
pytest-timeout==1.0.0
45
mock
56
fake-factory
67
sure

tests/integration/test_main.py

Lines changed: 148 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import caduc.timer
2+
import caduc.image
23
import docker
34
import docker.utils
5+
import docker.errors
6+
import logging
7+
import pytest
48
import sys
59
import time
6-
import threading
10+
import sure
711
import unittest
812

913
from caduc.cmd import create_watcher
@@ -17,19 +21,30 @@
1721
no_docker = True
1822

1923

20-
class WatchThread(threading.Thread):
24+
class ControlledTimer(object):
25+
def __init__(self, delay, cb):
26+
self.cb = cb
27+
self.delay = delay
28+
self.started = False
2129

22-
def __init__(self, watcher):
23-
self.watcher = watcher
24-
super(WatchThread, self).__init__()
30+
def start(self):
31+
self.started = True
2532

26-
def run(self):
27-
self.watcher.watch()
33+
def cancel(self):
34+
self.started = False
2835

36+
def _trigger(self):
37+
if not self.started:
38+
raise RuntimeError("Cannot trigger a non started timer on %r" % self.cb)
39+
self.cb()
40+
41+
def __str__(self):
42+
return "<Timer: delay: %s started: %s>" % (self.delay, self.started)
2943

3044
@unittest.skipIf(no_docker, "Failed to connect to docker host, error: %s" % e)
3145
class IntegrationTest(unittest.TestCase):
3246
def setUp(self):
47+
self.logger = logging.getLogger(type(self).__name__)
3348
self.client = docker.Client(**docker.utils.kwargs_from_env(assert_hostname=False))
3449
options = mock.Mock()
3550
options.debug = False
@@ -42,67 +57,142 @@ def setUp(self):
4257

4358
def tearDown(self):
4459
caduc.timer.Timer.CancelAll()
45-
self.watchThread._Thread__stop()
4660
for container in self.containers :
4761
try:
48-
self.client.remove_container(container)
62+
self.client.remove_container(container,
63+
v=True,
64+
force=True
65+
)
4966
except:
5067
pass
5168
for image in self.images:
5269
try:
53-
self.client.remove_image(image)
70+
self.client.remove_image(image,
71+
force=True
72+
)
5473
except:
5574
pass
5675

57-
def start_watch(self):
58-
self.watcher = create_watcher(self.options, [])
59-
self.watchThread = WatchThread(self.watcher)
60-
self.watchThread.start()
61-
62-
def wait_for(self, f, expectation):
63-
value = f()
64-
for i in range(20):
65-
if f()==expectation:
66-
break
67-
time.sleep(0.05)
68-
else:
69-
raise AssertionError("Failed to match %s with expectation: (== %s)" % (value, expectation))
70-
71-
def wait_for_not(self, f, expectation):
72-
value = f()
73-
for i in range(20):
74-
if f()!=expectation:
75-
break
76-
time.sleep(0.05)
77-
else:
78-
raise AssertionError("Failed to match %s with expectation: (!= %s)" % (value, expectation))
79-
80-
def test_image_requirement_is_monitored_and_deleted(self):
81-
self.start_watch()
82-
for line in self.client.build("tests/fixtures/images", 'test-image-build'):
76+
def build_test_image(self, image_name):
77+
for line in self.client.build("tests/fixtures/images", image_name):
8378
sys.stdout.write(line)
84-
def get(dct, key):
85-
try:
86-
return dct[key]
87-
except KeyError:
88-
return None
89-
self.wait_for_not(lambda: get(self.watcher.images, 'test-image-build'), None)
90-
self.wait_for_not(lambda: self.watcher.images['test-image-build'].event, None)
79+
self.images.add(image_name)
9180

81+
def start_test_container(self, image_name):
9282
container = self.client.create_container('test-image-build', command='tail -f /dev/null', tty=True)
9383
self.containers.add(container['Id'])
94-
self.wait_for_not(lambda: get(self.watcher.containers, container['Id']), None)
95-
self.wait_for(lambda: self.watcher.images['test-image-build'].event, None)
96-
97-
self.client.remove_container(container['Id'])
98-
self.containers.remove(container['Id'])
99-
container_removal = time.time()
100-
101-
self.wait_for(lambda: get(self.watcher.containers, container['Id']), None)
102-
self.wait_for_not(lambda: get(self.watcher.images, 'test-image-build'), None)
103-
104-
self.wait_for_not(lambda: self.watcher.images['test-image-build'].event, None)
105-
106-
self.wait_for(lambda: get(self.watcher.images, 'test-image-build'), None)
107-
self.assertAlmostEqual(time.time() - container_removal, 1, places=0)
108-
84+
return container
85+
86+
def remove_test_container(self, container):
87+
self.client.remove_container(container,
88+
v=True,
89+
force=True
90+
)
91+
try:
92+
if isinstance(container, dict):
93+
self.containers.remove(container['Id'])
94+
else:
95+
self.containers.remove(container)
96+
except:
97+
pass
98+
99+
def dict_intersect(self, d1, d2):
100+
"""
101+
Returns the shared definition of 2 dicts
102+
"""
103+
common_keys = set(d1.keys()) & set(d2.keys())
104+
r = {}
105+
for key in common_keys:
106+
if isinstance(d1[key], dict) and isinstance(d2[key], dict):
107+
r[key] = self.dict_intersect(d1[key], d2[key])
108+
else:
109+
if d1[key] == d2[key]:
110+
r[key] = d1[key]
111+
return r
112+
113+
def wait_for_event(self, listener, watcher, event):
114+
for e in listener:
115+
watcher.handle(e)
116+
common = self.dict_intersect(e,event)
117+
self.logger.info('event: %r, waiting for: %r, shared keys: %r', e, event, common)
118+
if common == event:
119+
return
120+
121+
@mock.patch('caduc.image.Image.Timer', new=ControlledTimer)
122+
@pytest.mark.timeout(5)
123+
def test_image_tag_plans_image_deletion(self):
124+
watcher = create_watcher(self.options, [])
125+
listener = self.client.events(decode=True)
126+
self.build_test_image('test-image-build')
127+
self.wait_for_event(listener, watcher, {
128+
'Action':'tag',
129+
'Actor': {
130+
'Attributes':{
131+
'name':'test-image-build:latest'
132+
}
133+
}
134+
}
135+
)
136+
watcher.images['test-image-build'].event.should.not_be(None)
137+
watcher.images['test-image-build'].event.started.should.be.truthy
138+
watcher.images['test-image-build'].event.delay.should.be.eql(1)
139+
140+
@mock.patch('caduc.image.Image.Timer', new=ControlledTimer)
141+
@pytest.mark.timeout(5)
142+
def test_existing_image_deletion_is_planned(self):
143+
self.build_test_image('test-image-build')
144+
watcher = create_watcher(self.options, [])
145+
self.logger.info(watcher.images['test-image-build'])
146+
watcher.images['test-image-build'].event.should.not_be(None)
147+
watcher.images['test-image-build'].event.started.should.be.truthy
148+
149+
@mock.patch('caduc.image.Image.Timer', new=ControlledTimer)
150+
@pytest.mark.timeout(5)
151+
def test_container_creation_cancels_image_deletion(self):
152+
self.build_test_image('test-image-build')
153+
watcher = create_watcher(self.options, [])
154+
old_event = watcher.images['test-image-build'].event
155+
156+
listener = self.client.events(decode=True)
157+
container = self.start_test_container('test-image-build')
158+
159+
self.wait_for_event(listener, watcher, {
160+
'Action': 'create',
161+
'Type': 'container',
162+
})
163+
164+
old_event.started.should.not_be.truthy
165+
watcher.images['test-image-build'].event.should.be(None)
166+
167+
@mock.patch('caduc.image.Image.Timer', new=ControlledTimer)
168+
@pytest.mark.timeout(5)
169+
def test_container_removal_schedules_image_removal(self):
170+
self.build_test_image('test-image-build')
171+
172+
container = self.start_test_container('test-image-build')
173+
174+
listener = self.client.events(decode=True)
175+
watcher = create_watcher(self.options, [])
176+
self.remove_test_container(container)
177+
178+
self.wait_for_event(listener, watcher, {
179+
'Action': 'destroy',
180+
})
181+
182+
watcher.images['test-image-build'].event.should.not_be(None)
183+
watcher.images['test-image-build'].event.started.should.be.truthy
184+
watcher.images['test-image-build'].event.delay.should.eql(1)
185+
186+
@mock.patch('caduc.image.Image.Timer', new=ControlledTimer)
187+
@pytest.mark.timeout(5)
188+
def test_container_removal_schedules_image_removal(self):
189+
self.build_test_image('test-image-build')
190+
191+
listener = self.client.events(decode=True)
192+
watcher = create_watcher(self.options, [])
193+
194+
watcher.images['test-image-build'].event._trigger()
195+
196+
self.wait_for_event(listener, watcher, {
197+
'Action': 'delete',
198+
})

0 commit comments

Comments
 (0)