@@ -81,62 +81,87 @@ def test_repr(self):
81
81
class TestLightManagerLightAccess :
82
82
"""Test light access and management."""
83
83
84
- @patch ('busylight.manager.Light' )
85
- def test_lights_property_calls_all_lights (self , mock_light ):
84
+ def _make_sortable_mock_lights (self , count ):
85
+ """Create mock lights that can be sorted."""
86
+ lights = []
87
+ for i in range (count ):
88
+ # Create a simple sortable object
89
+ class SortableMock :
90
+ def __init__ (self , sort_key ):
91
+ self .sort_key = sort_key
92
+ def __lt__ (self , other ):
93
+ return self .sort_key < other .sort_key
94
+ def __eq__ (self , other ):
95
+ return self .sort_key == other .sort_key
96
+
97
+ light = SortableMock (i )
98
+ lights .append (light )
99
+ return lights
100
+
101
+ def test_lights_property_calls_all_lights (self ):
86
102
"""Should call lightclass.all_lights() when accessing lights."""
87
- mock_light .all_lights .return_value = [Mock (), Mock ()]
88
- manager = LightManager ()
103
+ mock_lights = self ._make_sortable_mock_lights (2 )
104
+
105
+ # Create a mock class that can pass isinstance(value, type) check
106
+ mock_light_class = type ('MockLightClass' , (), {})
107
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
108
+
109
+ manager = LightManager (lightclass = mock_light_class )
89
110
90
111
lights = manager .lights
91
- mock_light .all_lights .assert_called_once_with (reset = False )
112
+ mock_light_class .all_lights .assert_called_once_with (reset = False )
92
113
assert len (lights ) == 2
93
114
94
- @patch ('busylight.manager.Light' )
95
- def test_lights_property_caches_result (self , mock_light ):
115
+ def test_lights_property_caches_result (self ):
96
116
"""Should cache lights list after first call."""
97
- mock_lights = [Mock (), Mock ()]
98
- mock_light .all_lights .return_value = mock_lights
99
- manager = LightManager ()
117
+ mock_lights = self ._make_sortable_mock_lights (2 )
118
+ mock_light_class = type ('MockLightClass' , (), {})
119
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
120
+
121
+ manager = LightManager (lightclass = mock_light_class )
100
122
101
123
# First call
102
124
lights1 = manager .lights
103
- # Second call
125
+ # Second call
104
126
lights2 = manager .lights
105
127
106
128
# Should only call all_lights once
107
- mock_light .all_lights .assert_called_once ()
129
+ mock_light_class .all_lights .assert_called_once ()
108
130
assert lights1 is lights2
109
131
110
- @patch ('busylight.manager.Light' )
111
- def test_selected_lights_with_empty_indices (self , mock_light ):
132
+ def test_selected_lights_with_empty_indices (self ):
112
133
"""Empty indices should return all lights."""
113
- mock_lights = [Mock (), Mock ()]
114
- mock_light .all_lights .return_value = mock_lights
115
- manager = LightManager ()
134
+ mock_lights = self ._make_sortable_mock_lights (2 )
135
+ mock_light_class = type ('MockLightClass' , (), {})
136
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
137
+
138
+ manager = LightManager (lightclass = mock_light_class )
116
139
117
140
result = manager .selected_lights ([])
118
- assert result == mock_lights
141
+ assert len ( result ) == 2
119
142
120
143
result = manager .selected_lights (None )
121
- assert result == mock_lights
144
+ assert len ( result ) == 2
122
145
123
- @patch ('busylight.manager.Light' )
124
- def test_selected_lights_with_valid_indices (self , mock_light ):
146
+ def test_selected_lights_with_valid_indices (self ):
125
147
"""Should return lights at specified indices."""
126
- mock_lights = [Mock (), Mock (), Mock ()]
127
- mock_light .all_lights .return_value = mock_lights
128
- manager = LightManager ()
148
+ mock_lights = self ._make_sortable_mock_lights (3 )
149
+ mock_light_class = type ('MockLightClass' , (), {})
150
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
151
+
152
+ manager = LightManager (lightclass = mock_light_class )
129
153
130
154
result = manager .selected_lights ([0 , 2 ])
131
- assert result == [ mock_lights [ 0 ], mock_lights [ 2 ]]
155
+ assert len ( result ) == 2
132
156
133
- @patch ('busylight.manager.Light' )
134
- def test_selected_lights_with_invalid_index_raises_error (self , mock_light ):
157
+ def test_selected_lights_with_invalid_index_raises_error (self ):
135
158
"""Invalid indices should raise NoLightsFoundError."""
136
159
from busylight_core import NoLightsFoundError
137
- mock_lights = [Mock ()]
138
- mock_light .all_lights .return_value = mock_lights
139
- manager = LightManager ()
160
+ mock_lights = self ._make_sortable_mock_lights (1 )
161
+ mock_light_class = type ('MockLightClass' , (), {})
162
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
163
+
164
+ manager = LightManager (lightclass = mock_light_class )
140
165
141
166
with pytest .raises (NoLightsFoundError ):
142
167
manager .selected_lights ([5 ])
@@ -160,3 +185,175 @@ def test_str_representation(self):
160
185
assert "Light 2" in str_result
161
186
assert "0" in str_result # indices
162
187
assert "1" in str_result
188
+
189
+
190
+ class TestLightManagerMethods :
191
+ """Test additional LightManager methods for coverage."""
192
+
193
+ def test_update_greedy_true (self ):
194
+ """Should update lights with greedy=True."""
195
+ mock_light_class = type ('MockLightClass' , (), {})
196
+ mock_existing_lights = self ._make_sortable_mock_lights (2 )
197
+ mock_new_lights = self ._make_sortable_mock_lights (1 )
198
+ mock_light_class .all_lights = Mock (side_effect = [mock_existing_lights , mock_new_lights ])
199
+
200
+ manager = LightManager (lightclass = mock_light_class )
201
+
202
+ # Set up existing lights with plugged/unplugged status
203
+ mock_existing_lights [0 ].is_pluggedin = True
204
+ mock_existing_lights [0 ].is_unplugged = False
205
+ mock_existing_lights [1 ].is_pluggedin = False
206
+ mock_existing_lights [1 ].is_unplugged = True
207
+
208
+ # Initialize lights first
209
+ _ = manager .lights
210
+
211
+ # Now call update
212
+ new_count , active_count , inactive_count = manager .update (greedy = True )
213
+
214
+ assert new_count == 1 # Added 1 new light
215
+ assert active_count == 1 # 1 plugged in light
216
+ assert inactive_count == 1 # 1 unplugged light
217
+
218
+ # Should have called all_lights again for new lights
219
+ assert mock_light_class .all_lights .call_count == 2
220
+
221
+ def test_update_greedy_false (self ):
222
+ """Should update lights with greedy=False."""
223
+ mock_light_class = type ('MockLightClass' , (), {})
224
+ mock_existing_lights = self ._make_sortable_mock_lights (1 )
225
+ mock_light_class .all_lights = Mock (return_value = mock_existing_lights )
226
+
227
+ manager = LightManager (lightclass = mock_light_class )
228
+
229
+ # Set up existing lights with plugged status
230
+ mock_existing_lights [0 ].is_pluggedin = True
231
+ mock_existing_lights [0 ].is_unplugged = False
232
+
233
+ # Initialize lights first
234
+ _ = manager .lights
235
+
236
+ # Now call update with greedy=False
237
+ new_count , active_count , inactive_count = manager .update (greedy = False )
238
+
239
+ assert new_count == 0 # No new lights added
240
+ assert active_count == 1 # 1 active light
241
+ assert inactive_count == 0 # 0 inactive lights
242
+
243
+ # Should only call all_lights once (during initialization)
244
+ assert mock_light_class .all_lights .call_count == 1
245
+
246
+ def test_release_with_lights (self ):
247
+ """Should release managed lights."""
248
+ mock_light_class = type ('MockLightClass' , (), {})
249
+ mock_lights = self ._make_sortable_mock_lights (2 )
250
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
251
+
252
+ manager = LightManager (lightclass = mock_light_class )
253
+
254
+ # Initialize lights
255
+ _ = manager .lights
256
+ assert len (manager .lights ) == 2
257
+
258
+ # Release lights
259
+ manager .release ()
260
+
261
+ # Should clear lights list
262
+ assert len (manager ._lights ) == 0
263
+
264
+ def test_release_empty_lights (self ):
265
+ """Should handle release when no lights."""
266
+ manager = LightManager ()
267
+
268
+ # Should not raise error
269
+ manager .release ()
270
+
271
+ def test_on_method_success (self ):
272
+ """Should turn on lights successfully."""
273
+ mock_light_class = type ('MockLightClass' , (), {})
274
+ mock_lights = self ._make_sortable_mock_lights (2 )
275
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
276
+
277
+ manager = LightManager (lightclass = mock_light_class )
278
+
279
+ # Mock the selected_lights and lights to have on() method
280
+ for light in mock_lights :
281
+ light .on = Mock ()
282
+
283
+ # Call on method
284
+ manager .on (color = (255 , 0 , 0 ), light_ids = [0 , 1 ])
285
+
286
+ # Should call on() for each light
287
+ for light in mock_lights :
288
+ light .on .assert_called_once_with ((255 , 0 , 0 ))
289
+
290
+ def test_on_method_with_timeout (self ):
291
+ """Should handle timeout in on method."""
292
+ mock_light_class = type ('MockLightClass' , (), {})
293
+ mock_lights = self ._make_sortable_mock_lights (1 )
294
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
295
+
296
+ manager = LightManager (lightclass = mock_light_class )
297
+
298
+ # Mock light on method
299
+ mock_lights [0 ].on = Mock ()
300
+
301
+ # Call on method with timeout
302
+ manager .on (color = (0 , 255 , 0 ), light_ids = [0 ], timeout = 5.0 )
303
+
304
+ # Should still call on method
305
+ mock_lights [0 ].on .assert_called_once_with ((0 , 255 , 0 ))
306
+
307
+ def test_off_method (self ):
308
+ """Should turn off lights."""
309
+ mock_light_class = type ('MockLightClass' , (), {})
310
+ mock_lights = self ._make_sortable_mock_lights (1 )
311
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
312
+
313
+ manager = LightManager (lightclass = mock_light_class )
314
+
315
+ # Mock light off method
316
+ mock_lights [0 ].off = Mock ()
317
+
318
+ # Call off method
319
+ manager .off (lights = [0 ])
320
+
321
+ # Should call off() for the light
322
+ mock_lights [0 ].off .assert_called_once ()
323
+
324
+ @patch ('busylight.manager.asyncio' )
325
+ def test_apply_effect_success (self , mock_asyncio ):
326
+ """Should apply effect to lights."""
327
+ mock_light_class = type ('MockLightClass' , (), {})
328
+ mock_lights = self ._make_sortable_mock_lights (1 )
329
+ mock_light_class .all_lights = Mock (return_value = mock_lights )
330
+
331
+ # Create manager
332
+ manager = LightManager (lightclass = mock_light_class )
333
+
334
+ # Create mock effect
335
+ mock_effect = Mock ()
336
+ mock_effect .default_interval = 0.5
337
+
338
+ # Call apply_effect
339
+ manager .apply_effect (mock_effect , light_ids = [0 ])
340
+
341
+ # Should call asyncio.run
342
+ mock_asyncio .run .assert_called_once ()
343
+
344
+ def _make_sortable_mock_lights (self , count ):
345
+ """Create mock lights that can be sorted."""
346
+ lights = []
347
+ for i in range (count ):
348
+ # Create a simple sortable object
349
+ class SortableMock :
350
+ def __init__ (self , sort_key ):
351
+ self .sort_key = sort_key
352
+ def __lt__ (self , other ):
353
+ return self .sort_key < other .sort_key
354
+ def __eq__ (self , other ):
355
+ return self .sort_key == other .sort_key
356
+
357
+ light = SortableMock (i )
358
+ lights .append (light )
359
+ return lights
0 commit comments