1
1
import caduc .timer
2
+ import caduc .image
2
3
import docker
3
4
import docker .utils
5
+ import docker .errors
6
+ import logging
7
+ import pytest
4
8
import sys
5
9
import time
6
- import threading
10
+ import sure
7
11
import unittest
8
12
9
13
from caduc .cmd import create_watcher
17
21
no_docker = True
18
22
19
23
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
21
29
22
- def __init__ (self , watcher ):
23
- self .watcher = watcher
24
- super (WatchThread , self ).__init__ ()
30
+ def start (self ):
31
+ self .started = True
25
32
26
- def run (self ):
27
- self .watcher . watch ()
33
+ def cancel (self ):
34
+ self .started = False
28
35
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 )
29
43
30
44
@unittest .skipIf (no_docker , "Failed to connect to docker host, error: %s" % e )
31
45
class IntegrationTest (unittest .TestCase ):
32
46
def setUp (self ):
47
+ self .logger = logging .getLogger (type (self ).__name__ )
33
48
self .client = docker .Client (** docker .utils .kwargs_from_env (assert_hostname = False ))
34
49
options = mock .Mock ()
35
50
options .debug = False
@@ -42,67 +57,142 @@ def setUp(self):
42
57
43
58
def tearDown (self ):
44
59
caduc .timer .Timer .CancelAll ()
45
- self .watchThread ._Thread__stop ()
46
60
for container in self .containers :
47
61
try :
48
- self .client .remove_container (container )
62
+ self .client .remove_container (container ,
63
+ v = True ,
64
+ force = True
65
+ )
49
66
except :
50
67
pass
51
68
for image in self .images :
52
69
try :
53
- self .client .remove_image (image )
70
+ self .client .remove_image (image ,
71
+ force = True
72
+ )
54
73
except :
55
74
pass
56
75
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 ):
83
78
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 )
91
80
81
+ def start_test_container (self , image_name ):
92
82
container = self .client .create_container ('test-image-build' , command = 'tail -f /dev/null' , tty = True )
93
83
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