1- # Copyright 2019-2021 , Optimizely
1+ # Copyright 2019-2022 , Optimizely
22# Licensed under the Apache License, Version 2.0 (the "License");
33# you may not use this file except in compliance with the License.
44# You may obtain a copy of the License at
@@ -218,6 +218,38 @@ def test_get_config_blocks(self):
218218 self .assertEqual (1 , round (end_time - start_time ))
219219
220220
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+
221253@mock .patch ('requests.get' )
222254class PollingConfigManagerTest (base .BaseTest ):
223255 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, _):
294326
295327 def test_set_update_interval (self , _ ):
296328 """ 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 :
298333 project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
299334
335+ mock_thread .assert_called_once ()
300336 # Assert that if invalid update_interval is set, then exception is raised.
301337 with self .assertRaisesRegex (
302338 optimizely_exceptions .InvalidInputException , 'Invalid update_interval "invalid interval" provided.' ,
@@ -321,9 +357,13 @@ def test_set_update_interval(self, _):
321357
322358 def test_set_blocking_timeout (self , _ ):
323359 """ 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 :
325364 project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
326365
366+ mock_thread .assert_called_once ()
327367 # Assert that if invalid blocking_timeout is set, then exception is raised.
328368 with self .assertRaisesRegex (
329369 optimizely_exceptions .InvalidInputException , 'Invalid blocking timeout "invalid timeout" provided.' ,
@@ -352,9 +392,13 @@ def test_set_blocking_timeout(self, _):
352392
353393 def test_set_last_modified (self , _ ):
354394 """ 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 :
356399 project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
357400
401+ mock_thread .assert_called_once ()
358402 last_modified_time = 'Test Last Modified Time'
359403 test_response_headers = {
360404 'Last-Modified' : last_modified_time ,
@@ -366,24 +410,40 @@ def test_set_last_modified(self, _):
366410 def test_fetch_datafile (self , _ ):
367411 """ Test that fetch_datafile sets config and last_modified based on response. """
368412 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 )
371418 expected_datafile_url = enums .ConfigManager .DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
372419 test_headers = {'Last-Modified' : 'New Time' }
373420 test_datafile = json .dumps (self .config_dict_with_features )
374421 test_response = requests .Response ()
375422 test_response .status_code = 200
376423 test_response .headers = test_headers
377424 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
380431
432+ mock_request .assert_called_once_with (
433+ expected_datafile_url ,
434+ headers = {},
435+ timeout = enums .ConfigManager .REQUEST_TIMEOUT
436+ )
381437 self .assertEqual (test_headers ['Last-Modified' ], project_config_manager .last_modified )
382438 self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
383439
384440 # Call fetch_datafile again and assert that request to URL is with If-Modified-Since header.
385441 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
387447
388448 mock_requests .assert_called_once_with (
389449 expected_datafile_url ,
@@ -394,6 +454,9 @@ def test_fetch_datafile(self, _):
394454 self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
395455 self .assertTrue (project_config_manager .is_running )
396456
457+ # Shut down the polling thread
458+ project_config_manager .stop = True
459+
397460 def test_fetch_datafile__status_exception_raised (self , _ ):
398461 """ Test that config_manager keeps running if status code exception is raised when fetching datafile. """
399462 class MockExceptionResponse (object ):
@@ -402,24 +465,40 @@ def raise_for_status(self):
402465
403466 sdk_key = 'some_key'
404467 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 )
407468 expected_datafile_url = enums .ConfigManager .DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
408469 test_headers = {'Last-Modified' : 'New Time' }
409470 test_datafile = json .dumps (self .config_dict_with_features )
410471 test_response = requests .Response ()
411472 test_response .status_code = 200
412473 test_response .headers = test_headers
413474 test_response ._content = test_datafile
414- with mock .patch ('requests.get' , return_value = test_response ):
415- project_config_manager .fetch_datafile ()
416475
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+ )
417492 self .assertEqual (test_headers ['Last-Modified' ], project_config_manager .last_modified )
418493 self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
419494
420495 # Call fetch_datafile again, but raise exception this time
421496 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
423502
424503 mock_requests .assert_called_once_with (
425504 expected_datafile_url ,
@@ -434,22 +513,37 @@ def raise_for_status(self):
434513 # Confirm that config manager keeps running
435514 self .assertTrue (project_config_manager .is_running )
436515
516+ # Shut down the polling thread
517+ project_config_manager .stop = True
518+
437519 def test_fetch_datafile__request_exception_raised (self , _ ):
438520 """ Test that config_manager keeps running if a request exception is raised when fetching datafile. """
439521 sdk_key = 'some_key'
440522 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 )
443528 expected_datafile_url = enums .ConfigManager .DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
444529 test_headers = {'Last-Modified' : 'New Time' }
445530 test_datafile = json .dumps (self .config_dict_with_features )
446531 test_response = requests .Response ()
447532 test_response .status_code = 200
448533 test_response .headers = test_headers
449534 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
452541
542+ mock_request .assert_called_once_with (
543+ expected_datafile_url ,
544+ headers = {},
545+ timeout = enums .ConfigManager .REQUEST_TIMEOUT
546+ )
453547 self .assertEqual (test_headers ['Last-Modified' ], project_config_manager .last_modified )
454548 self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
455549
@@ -458,7 +552,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
458552 'requests.get' ,
459553 side_effect = requests .exceptions .RequestException ('Error Error !!' ),
460554 ) 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
462560
463561 mock_requests .assert_called_once_with (
464562 expected_datafile_url ,
@@ -473,12 +571,18 @@ def test_fetch_datafile__request_exception_raised(self, _):
473571 # Confirm that config manager keeps running
474572 self .assertTrue (project_config_manager .is_running )
475573
574+ # Shut down the polling thread
575+ project_config_manager .stop = True
576+
476577 def test_is_running (self , _ ):
477578 """ Test that polling thread is running after instance of PollingConfigManager is created. """
478579 with mock .patch ('optimizely.config_manager.PollingConfigManager.fetch_datafile' ):
479580 project_config_manager = config_manager .PollingConfigManager (sdk_key = 'some_key' )
480581 self .assertTrue (project_config_manager .is_running )
481582
583+ # Prevent the polling thread from running fetch_datafile if it hasn't already
584+ project_config_manager ._polling_thread ._is_stopped = True
585+
482586
483587@mock .patch ('requests.get' )
484588class AuthDatafilePollingConfigManagerTest (base .BaseTest ):
@@ -495,10 +599,14 @@ def test_set_datafile_access_token(self, _):
495599 """ Test that datafile_access_token is properly set as instance variable. """
496600 datafile_access_token = 'some_token'
497601 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 :
499606 project_config_manager = config_manager .AuthDatafilePollingConfigManager (
500607 datafile_access_token = datafile_access_token , sdk_key = sdk_key )
501608
609+ mock_thread .assert_called_once ()
502610 self .assertEqual (datafile_access_token , project_config_manager .datafile_access_token )
503611
504612 def test_fetch_datafile (self , _ ):
@@ -538,9 +646,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
538646 sdk_key = 'some_key'
539647 mock_logger = mock .Mock ()
540648
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 )
544654 expected_datafile_url = enums .ConfigManager .AUTHENTICATED_DATAFILE_URL_TEMPLATE .format (sdk_key = sdk_key )
545655 test_headers = {'Last-Modified' : 'New Time' }
546656 test_datafile = json .dumps (self .config_dict_with_features )
@@ -552,7 +662,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
552662 # Call fetch_datafile and assert that request was sent with correct authorization header
553663 with mock .patch ('requests.get' ,
554664 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
556670
557671 mock_request .assert_called_once_with (
558672 expected_datafile_url ,
@@ -568,7 +682,11 @@ def test_fetch_datafile__request_exception_raised(self, _):
568682 'requests.get' ,
569683 side_effect = requests .exceptions .RequestException ('Error Error !!' ),
570684 ) 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
572690
573691 mock_requests .assert_called_once_with (
574692 expected_datafile_url ,
@@ -586,3 +704,6 @@ def test_fetch_datafile__request_exception_raised(self, _):
586704 self .assertIsInstance (project_config_manager .get_config (), project_config .ProjectConfig )
587705 # Confirm that config manager keeps running
588706 self .assertTrue (project_config_manager .is_running )
707+
708+ # Shut down the polling thread
709+ project_config_manager .stop = True
0 commit comments