1
- # Copyright 2019-2021 , Optimizely
1
+ # Copyright 2019-2022 , Optimizely
2
2
# Licensed under the Apache License, Version 2.0 (the "License");
3
3
# you may not use this file except in compliance with the License.
4
4
# You may obtain a copy of the License at
@@ -218,6 +218,38 @@ def test_get_config_blocks(self):
218
218
self .assertEqual (1 , round (end_time - start_time ))
219
219
220
220
221
+ class MockPollingConfigManager (config_manager .PollingConfigManager ):
222
+ ''' Wrapper class to allow manual call of fetch_datafile in the polling thread by
223
+ overriding the _run method.'''
224
+ def __init__ (self , * args , ** kwargs ):
225
+ self .run = False
226
+ self .stop = False
227
+ super ().__init__ (* args , ** kwargs )
228
+
229
+ def _run (self ):
230
+ '''Parent thread can use self.run to start fetch_datafile in polling thread and wait for it to complete.'''
231
+ while self .is_running and not self .stop :
232
+ if self .run :
233
+ self .fetch_datafile ()
234
+ self .run = False
235
+
236
+
237
+ class MockAuthDatafilePollingConfigManager (config_manager .AuthDatafilePollingConfigManager ):
238
+ ''' Wrapper class to allow manual call of fetch_datafile in the polling thread by
239
+ overriding the _run method.'''
240
+ def __init__ (self , * args , ** kwargs ):
241
+ self .run = False
242
+ self .stop = False
243
+ super ().__init__ (* args , ** kwargs )
244
+
245
+ def _run (self ):
246
+ '''Parent thread can use self.run to start fetch_datafile and wait for it to complete.'''
247
+ while self .is_running and not self .stop :
248
+ if self .run :
249
+ self .fetch_datafile ()
250
+ self .run = False
251
+
252
+
221
253
@mock .patch ('requests.get' )
222
254
class PollingConfigManagerTest (base .BaseTest ):
223
255
def test_init__no_sdk_key_no_url__fails (self , _ ):
@@ -294,9 +326,13 @@ def test_get_datafile_url__sdk_key_and_url_and_template_provided(self, _):
294
326
295
327
def test_set_update_interval (self , _ ):
296
328
""" Test set_update_interval with different inputs. """
297
- with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
329
+
330
+ # prevent polling thread from starting in PollingConfigManager.__init__
331
+ # otherwise it can outlive this test and get out of sync with pytest
332
+ with mock .patch ('threading.Thread.start' ) as mock_thread :
298
333
project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
299
334
335
+ mock_thread .assert_called_once ()
300
336
# Assert that if invalid update_interval is set, then exception is raised.
301
337
with self .assertRaisesRegex (
302
338
optimizely_exceptions .InvalidInputException , 'Invalid update_interval "invalid interval" provided.' ,
@@ -321,9 +357,13 @@ def test_set_update_interval(self, _):
321
357
322
358
def test_set_blocking_timeout (self , _ ):
323
359
""" Test set_blocking_timeout with different inputs. """
324
- with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
360
+
361
+ # prevent polling thread from starting in PollingConfigManager.__init__
362
+ # otherwise it can outlive this test and get out of sync with pytest
363
+ with mock .patch ('threading.Thread.start' ) as mock_thread :
325
364
project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
326
365
366
+ mock_thread .assert_called_once ()
327
367
# Assert that if invalid blocking_timeout is set, then exception is raised.
328
368
with self .assertRaisesRegex (
329
369
optimizely_exceptions .InvalidInputException , 'Invalid blocking timeout "invalid timeout" provided.' ,
@@ -352,9 +392,13 @@ def test_set_blocking_timeout(self, _):
352
392
353
393
def test_set_last_modified (self , _ ):
354
394
""" Test that set_last_modified sets last_modified field based on header. """
355
- with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
395
+
396
+ # prevent polling thread from starting in PollingConfigManager.__init__
397
+ # otherwise it can outlive this test and get out of sync with pytest
398
+ with mock .patch ('threading.Thread.start' ) as mock_thread :
356
399
project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
357
400
401
+ mock_thread .assert_called_once ()
358
402
last_modified_time = 'Test Last Modified Time'
359
403
test_response_headers = {
360
404
'Last-Modified' : last_modified_time ,
@@ -366,24 +410,40 @@ def test_set_last_modified(self, _):
366
410
def test_fetch_datafile (self , _ ):
367
411
""" Test that fetch_datafile sets config and last_modified based on response. """
368
412
sdk_key = 'some_key'
369
- with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
370
- project_config_manager = config_manager .PollingConfigManager (sdk_key = sdk_key )
413
+
414
+ # use wrapper class to control start and stop of fetch_datafile
415
+ # this prevents the polling thread from outliving the test
416
+ # and getting out of sync with pytest
417
+ project_config_manager = MockPollingConfigManager (sdk_key = sdk_key )
371
418
expected_datafile_url = enums .ConfigManager .DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
372
419
test_headers = {'Last-Modified' : 'New Time' }
373
420
test_datafile = json .dumps (self .config_dict_with_features )
374
421
test_response = requests .Response ()
375
422
test_response .status_code = 200
376
423
test_response .headers = test_headers
377
424
test_response ._content = test_datafile
378
- with mock .patch ('requests.get' , return_value = test_response ):
379
- project_config_manager .fetch_datafile ()
425
+ with mock .patch ('requests.get' , return_value = test_response ) as mock_request :
426
+ # manually trigger fetch_datafile in the polling thread
427
+ project_config_manager .run = True
428
+ # Wait for polling thread to finish
429
+ while project_config_manager .run :
430
+ pass
380
431
432
+ mock_request .assert_called_once_with (
433
+ expected_datafile_url ,
434
+ headers = {},
435
+ timeout = enums .ConfigManager .REQUEST_TIMEOUT
436
+ )
381
437
self .assertEqual (test_headers ['Last-Modified' ], project_config_manager .last_modified )
382
438
self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
383
439
384
440
# Call fetch_datafile again and assert that request to URL is with If-Modified-Since header.
385
441
with mock .patch ('requests.get' , return_value = test_response ) as mock_requests :
386
- project_config_manager .fetch_datafile ()
442
+ # manually trigger fetch_datafile in the polling thread
443
+ project_config_manager .run = True
444
+ # Wait for polling thread to finish
445
+ while project_config_manager .run :
446
+ pass
387
447
388
448
mock_requests .assert_called_once_with (
389
449
expected_datafile_url ,
@@ -394,6 +454,9 @@ def test_fetch_datafile(self, _):
394
454
self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
395
455
self .assertTrue (project_config_manager .is_running )
396
456
457
+ # Shut down the polling thread
458
+ project_config_manager .stop = True
459
+
397
460
def test_fetch_datafile__status_exception_raised (self , _ ):
398
461
""" Test that config_manager keeps running if status code exception is raised when fetching datafile. """
399
462
class MockExceptionResponse (object ):
@@ -402,24 +465,40 @@ def raise_for_status(self):
402
465
403
466
sdk_key = 'some_key'
404
467
mock_logger = mock .Mock ()
405
- with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
406
- project_config_manager = config_manager .PollingConfigManager (sdk_key = sdk_key , logger = mock_logger )
407
468
expected_datafile_url = enums .ConfigManager .DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
408
469
test_headers = {'Last-Modified' : 'New Time' }
409
470
test_datafile = json .dumps (self .config_dict_with_features )
410
471
test_response = requests .Response ()
411
472
test_response .status_code = 200
412
473
test_response .headers = test_headers
413
474
test_response ._content = test_datafile
414
- with mock .patch ('requests.get' , return_value = test_response ):
415
- project_config_manager .fetch_datafile ()
416
475
476
+ # use wrapper class to control start and stop of fetch_datafile
477
+ # this prevents the polling thread from outliving the test
478
+ # and getting out of sync with pytest
479
+ project_config_manager = MockPollingConfigManager (sdk_key = sdk_key , logger = mock_logger )
480
+ with mock .patch ('requests.get' , return_value = test_response ) as mock_request :
481
+ # manually trigger fetch_datafile in the polling thread
482
+ project_config_manager .run = True
483
+ # Wait for polling thread to finish
484
+ while project_config_manager .run :
485
+ pass
486
+
487
+ mock_request .assert_called_once_with (
488
+ expected_datafile_url ,
489
+ headers = {},
490
+ timeout = enums .ConfigManager .REQUEST_TIMEOUT
491
+ )
417
492
self .assertEqual (test_headers ['Last-Modified' ], project_config_manager .last_modified )
418
493
self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
419
494
420
495
# Call fetch_datafile again, but raise exception this time
421
496
with mock .patch ('requests.get' , return_value = MockExceptionResponse ()) as mock_requests :
422
- project_config_manager .fetch_datafile ()
497
+ # manually trigger fetch_datafile in the polling thread
498
+ project_config_manager .run = True
499
+ # Wait for polling thread to finish
500
+ while project_config_manager .run :
501
+ pass
423
502
424
503
mock_requests .assert_called_once_with (
425
504
expected_datafile_url ,
@@ -434,22 +513,37 @@ def raise_for_status(self):
434
513
# Confirm that config manager keeps running
435
514
self .assertTrue (project_config_manager .is_running )
436
515
516
+ # Shut down the polling thread
517
+ project_config_manager .stop = True
518
+
437
519
def test_fetch_datafile__request_exception_raised (self , _ ):
438
520
""" Test that config_manager keeps running if a request exception is raised when fetching datafile. """
439
521
sdk_key = 'some_key'
440
522
mock_logger = mock .Mock ()
441
- with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
442
- project_config_manager = config_manager .PollingConfigManager (sdk_key = sdk_key , logger = mock_logger )
523
+
524
+ # use wrapper class to control start and stop of fetch_datafile
525
+ # this prevents the polling thread from outliving the test
526
+ # and getting out of sync with pytest
527
+ project_config_manager = MockPollingConfigManager (sdk_key = sdk_key , logger = mock_logger )
443
528
expected_datafile_url = enums .ConfigManager .DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
444
529
test_headers = {'Last-Modified' : 'New Time' }
445
530
test_datafile = json .dumps (self .config_dict_with_features )
446
531
test_response = requests .Response ()
447
532
test_response .status_code = 200
448
533
test_response .headers = test_headers
449
534
test_response ._content = test_datafile
450
- with mock .patch ('requests.get' , return_value = test_response ):
451
- project_config_manager .fetch_datafile ()
535
+ with mock .patch ('requests.get' , return_value = test_response ) as mock_request :
536
+ # manually trigger fetch_datafile in the polling thread
537
+ project_config_manager .run = True
538
+ # Wait for polling thread to finish
539
+ while project_config_manager .run :
540
+ pass
452
541
542
+ mock_request .assert_called_once_with (
543
+ expected_datafile_url ,
544
+ headers = {},
545
+ timeout = enums .ConfigManager .REQUEST_TIMEOUT
546
+ )
453
547
self .assertEqual (test_headers ['Last-Modified' ], project_config_manager .last_modified )
454
548
self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
455
549
@@ -458,7 +552,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
458
552
'requests.get' ,
459
553
side_effect = requests .exceptions .RequestException ('Error Error !!' ),
460
554
) as mock_requests :
461
- project_config_manager .fetch_datafile ()
555
+ # manually trigger fetch_datafile in the polling thread
556
+ project_config_manager .run = True
557
+ # Wait for polling thread to finish
558
+ while project_config_manager .run :
559
+ pass
462
560
463
561
mock_requests .assert_called_once_with (
464
562
expected_datafile_url ,
@@ -473,12 +571,18 @@ def test_fetch_datafile__request_exception_raised(self, _):
473
571
# Confirm that config manager keeps running
474
572
self .assertTrue (project_config_manager .is_running )
475
573
574
+ # Shut down the polling thread
575
+ project_config_manager .stop = True
576
+
476
577
def test_is_running (self , _ ):
477
578
""" Test that polling thread is running after instance of PollingConfigManager is created. """
478
579
with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
479
580
project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
480
581
self .assertTrue (project_config_manager .is_running )
481
582
583
+ # Prevent the polling thread from running fetch_datafile if it hasn't already
584
+ project_config_manager ._polling_thread ._is_stopped = True
585
+
482
586
483
587
@mock .patch ('requests.get' )
484
588
class AuthDatafilePollingConfigManagerTest (base .BaseTest ):
@@ -495,10 +599,14 @@ def test_set_datafile_access_token(self, _):
495
599
""" Test that datafile_access_token is properly set as instance variable. """
496
600
datafile_access_token = 'some_token'
497
601
sdk_key = 'some_key'
498
- with mock .patch ('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile' ):
602
+
603
+ # prevent polling thread from starting in PollingConfigManager.__init__
604
+ # otherwise it can outlive this test and get out of sync with pytest
605
+ with mock .patch ('threading.Thread.start' ) as mock_thread :
499
606
project_config_manager = config_manager .AuthDatafilePollingConfigManager (
500
607
datafile_access_token = datafile_access_token , sdk_key = sdk_key )
501
608
609
+ mock_thread .assert_called_once ()
502
610
self .assertEqual (datafile_access_token , project_config_manager .datafile_access_token )
503
611
504
612
def test_fetch_datafile (self , _ ):
@@ -538,9 +646,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
538
646
sdk_key = 'some_key'
539
647
mock_logger = mock .Mock ()
540
648
541
- with mock .patch ('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile' ):
542
- project_config_manager = config_manager .AuthDatafilePollingConfigManager (
543
- datafile_access_token = datafile_access_token , sdk_key = sdk_key , logger = mock_logger )
649
+ # use wrapper class to control start and stop of fetch_datafile
650
+ # this prevents the polling thread from outliving the test
651
+ # and getting out of sync with pytest
652
+ project_config_manager = MockAuthDatafilePollingConfigManager (datafile_access_token = datafile_access_token ,
653
+ sdk_key = sdk_key , logger = mock_logger )
544
654
expected_datafile_url = enums .ConfigManager .AUTHENTICATED_DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
545
655
test_headers = {'Last-Modified' : 'New Time' }
546
656
test_datafile = json .dumps (self .config_dict_with_features )
@@ -552,7 +662,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
552
662
# Call fetch_datafile and assert that request was sent with correct authorization header
553
663
with mock .patch ('requests.get' ,
554
664
return_value = test_response ) as mock_request :
555
- project_config_manager .fetch_datafile ()
665
+ # manually trigger fetch_datafile in the polling thread
666
+ project_config_manager .run = True
667
+ # Wait for polling thread to finish
668
+ while project_config_manager .run :
669
+ pass
556
670
557
671
mock_request .assert_called_once_with (
558
672
expected_datafile_url ,
@@ -568,7 +682,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
568
682
'requests.get' ,
569
683
side_effect = requests .exceptions .RequestException ('Error Error !!' ),
570
684
) as mock_requests :
571
- project_config_manager .fetch_datafile ()
685
+ # manually trigger fetch_datafile in the polling thread
686
+ project_config_manager .run = True
687
+ # Wait for polling thread to finish
688
+ while project_config_manager .run :
689
+ pass
572
690
573
691
mock_requests .assert_called_once_with (
574
692
expected_datafile_url ,
@@ -586,3 +704,6 @@ def test_fetch_datafile__request_exception_raised(self, _):
586
704
self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
587
705
# Confirm that config manager keeps running
588
706
self .assertTrue (project_config_manager .is_running )
707
+
708
+ # Shut down the polling thread
709
+ project_config_manager .stop = True
0 commit comments