@@ -415,8 +415,71 @@ def testOctalNumberOption(self):
415415 print (type (config ._get_option ('UMASK' )))
416416
417417
418+ @pytest .mark .usefixtures ("save_restore_logging" )
418419class TrackerConfig (unittest .TestCase ):
419420
421+ @pytest .fixture (scope = "class" )
422+ def save_restore_logging (self ):
423+ """Save logger state and try to restore it after all tests in
424+ this class have finished.
425+
426+ The primary test is testDictLoggerConfigViaJson which
427+ can change the loggers and break tests that depend on caplog
428+ """
429+ # Save logger state for root and roundup top level logger
430+ loggernames = ("" , "roundup" )
431+
432+ # The state attributes to save. Lists are shallow copied
433+ state_to_save = ("filters" , "handlers" , "level" , "propagate" )
434+
435+ logger_state = {}
436+ for name in loggernames :
437+ logger_state [name ] = {}
438+ roundup_logger = logging .getLogger (name )
439+
440+ for i in state_to_save :
441+ attr = getattr (roundup_logger , i )
442+ if isinstance (attr , list ):
443+ logger_state [name ][i ] = attr .copy ()
444+ else :
445+ logger_state [name ][i ] = getattr (roundup_logger , i )
446+
447+ # run all class tests here
448+ yield
449+
450+ # rip down all the loggers leaving the root logger reporting
451+ # to stdout.
452+ # otherwise logger config is leaking to other tests
453+ roundup_loggers = [logging .getLogger (name ) for name in
454+ logging .root .manager .loggerDict
455+ if name .startswith ("roundup" )]
456+
457+ # cribbed from configuration.py:init_loggers
458+ hdlr = logging .StreamHandler (sys .stdout )
459+ formatter = logging .Formatter (
460+ '%(asctime)s %(levelname)s %(message)s' )
461+ hdlr .setFormatter (formatter )
462+
463+ for logger in roundup_loggers :
464+ # no logging API to remove all existing handlers!?!
465+ for h in logger .handlers :
466+ h .close ()
467+ logger .removeHandler (h )
468+ logger .handlers = [hdlr ]
469+ logger .setLevel ("WARNING" )
470+ logger .propagate = True # important as caplog requires this
471+
472+ # Restore the info we stored before running tests
473+ for name in loggernames :
474+ local_logger = logging .getLogger (name )
475+ for attr in logger_state [name ]:
476+ setattr (local_logger , attr , logger_state [name ][attr ])
477+
478+ # reset logging as well
479+ from importlib import reload
480+ logging .shutdown ()
481+ reload (logging )
482+
420483 backend = 'anydbm'
421484
422485 def setUp (self ):
@@ -1139,22 +1202,8 @@ def testDictLoggerConfigViaJson(self):
11391202 }
11401203 """ )
11411204
1142- # save roundup logger state
1143- loggernames = ("" , "roundup" )
1144- logger_state = {}
1145- for name in loggernames :
1146- logger_state [name ] = {}
1147-
1148- roundup_logger = logging .getLogger ("roundup" )
1149- for i in ("filters" , "handlers" , "level" , "propagate" ):
1150- attr = getattr (roundup_logger , i )
1151- if isinstance (attr , list ):
1152- logger_state [name ][i ] = attr .copy ()
1153- else :
1154- logger_state [name ][i ] = getattr (roundup_logger , i )
1155-
1156- log_config_filename = self .instance .tracker_home \
1157- + "/_test_log_config.json"
1205+ log_config_filename = os .path .join (self .instance .tracker_home ,
1206+ "_test_log_config.json" )
11581207
11591208 # happy path
11601209 with open (log_config_filename , "w" ) as log_config_file :
@@ -1176,10 +1225,11 @@ def testDictLoggerConfigViaJson(self):
11761225 self .assertEqual (
11771226 cm .exception .args [0 ],
11781227 ('Error parsing json logging dict '
1179- '(_test_instance/_test_log_config.json ) near \n \n '
1228+ '(%s ) near \n \n '
11801229 '"version": 1, # only supported version\n \n Expecting '
11811230 'property name enclosed in double quotes: line 3 column 18.\n '
1182- 'Maybe bad inline comment, 3 spaces needed before #.' )
1231+ 'Maybe bad inline comment, 3 spaces needed before #.' %
1232+ log_config_filename )
11831233 )
11841234
11851235 # broken trailing , on last dict element
@@ -1198,9 +1248,10 @@ def testDictLoggerConfigViaJson(self):
11981248 self .assertEqual (
11991249 cm .exception .args [0 ],
12001250 ('Error parsing json logging dict '
1201- '(_test_instance/_test_log_config.json) near \n \n '
1202- ' }\n \n Expecting property name enclosed in double '
1203- 'quotes: line 37 column 6.' )
1251+ '(%s) near \n \n '
1252+ ' }\n \n '
1253+ 'Expecting property name enclosed in double '
1254+ 'quotes: line 37 column 6.' % log_config_filename )
12041255 )
12051256
12061257 # 3.13+ diags FIXME
@@ -1211,22 +1262,12 @@ def testDictLoggerConfigViaJson(self):
12111262 self.assertEqual(
12121263 cm.exception.args[0],
12131264 ('Error parsing json logging dict '
1214- '(_test_instance/_test_log_config.json) near \n \n '
1215- ' }\n \n Expecting property name enclosed in double '
1216- 'quotes: line 37 column 6.')
1265+ '(%s) near \n \n '
1266+ ' "stream": "ext://sys.stdout"\n \n '
1267+ 'Expecting property name enclosed in double '
1268+ 'quotes: line 37 column 6.' % log_config_filename)
12171269 )
12181270 '''
1219-
1220- '''
1221- # comment out as it breaks the logging config for caplog
1222- # on test_rest.py:testBadFormAttributeErrorException
1223- # for all rdbms backends.
1224- # the log ERROR check never gets any info
1225-
1226- # commenting out root logger in config doesn't make it work.
1227- # storing root logger and roundup logger state and restoring it
1228- # still fails.
1229-
12301271 # happy path for init_logging()
12311272
12321273 # verify preconditions
@@ -1269,9 +1310,10 @@ def testDictLoggerConfigViaJson(self):
12691310 cm .exception .args [0 ].replace (
12701311 "object\n " , "object, got 'int'\n " ),
12711312 ('Error loading logging dict from '
1272- '_test_instance/_test_log_config.json .\n '
1313+ '%s .\n '
12731314 "ValueError: Unable to configure formatter 'http'\n "
1274- "expected string or bytes-like object, got 'int'\n ")
1315+ "expected string or bytes-like object, got 'int'\n " %
1316+ log_config_filename )
12751317 )
12761318
12771319 # broken invalid level MANGO
@@ -1288,16 +1330,18 @@ def testDictLoggerConfigViaJson(self):
12881330 self .assertEqual (
12891331 cm .exception .args [0 ],
12901332 ("Error loading logging dict from "
1291- "_test_instance/_test_log_config.json .\n ValueError: "
1333+ "%s .\n ValueError: "
12921334 "Unable to configure logger 'roundup.hyperdb'\n Unknown level: "
1293- "'MANGO'\n ")
1335+ "'MANGO'\n " % log_config_filename )
12941336
12951337 )
12961338
12971339 # broken invalid output directory
12981340 test_config = config1 .replace (
12991341 ' "_test_instance/access.log"' ,
13001342 ' "not_a_test_instance/access.log"' )
1343+ access_filename = os .path .join ("not_a_test_instance" , "access.log" )
1344+
13011345 with open (log_config_filename , "w" ) as log_config_file :
13021346 log_config_file .write (test_config )
13031347
@@ -1307,51 +1351,22 @@ def testDictLoggerConfigViaJson(self):
13071351 config = self .db .config .init_logging ()
13081352
13091353 # error includes full path which is different on different
1310- # CI and dev platforms. So munge the path using re.sub.
1311- self.assertEqual(
1312- re.sub("directory: \' /.*not_a", 'directory: not_a' ,
1313- cm.exception.args[0]),
1314- ("Error loading logging dict from "
1315- "_test_instance/_test_log_config.json.\n "
1316- "ValueError: Unable to configure handler 'access'\n "
1317- "[Errno 2] No such file or directory: "
1318- "not_a_test_instance/access.log'\n "
1319- )
1320- )
1321-
1322- '''
1323- # rip down all the loggers leaving the root logger reporting
1324- # to stdout.
1325- # otherwise logger config is leaking to other tests
1326-
1327- roundup_loggers = [logging .getLogger (name ) for name in
1328- logging .root .manager .loggerDict
1329- if name .startswith ("roundup" )]
1330-
1331- # cribbed from configuration.py:init_loggers
1332- hdlr = logging .StreamHandler (sys .stdout )
1333- formatter = logging .Formatter (
1334- '%(asctime)s %(levelname)s %(message)s' )
1335- hdlr .setFormatter (formatter )
1336-
1337- for logger in roundup_loggers :
1338- # no logging API to remove all existing handlers!?!
1339- for h in logger .handlers :
1340- h .close ()
1341- logger .removeHandler (h )
1342- logger .handlers = [hdlr ]
1343- logger .setLevel ("DEBUG" )
1344- logger .propagate = True
1345-
1346- for name in loggernames :
1347- local_logger = logging .getLogger (name )
1348- for attr in logger_state [name ]:
1349- # if I restore handlers state for root logger
1350- # I break the test referenced above. -- WHY????
1351- if attr == "handlers" and name == "" : continue
1352- setattr (local_logger , attr , logger_state [name ][attr ])
1353-
1354- from importlib import reload
1355- logging .shutdown ()
1356- reload (logging )
1354+ # CI and dev platforms. So munge the path using re.sub and
1355+ # replace. Windows needs replace as the full path for windows
1356+ # to the file has '\\\\' not '\\' when taken from __context__.
1357+ # E.G.
1358+ # ("Error loading logging dict from '
1359+ # '_test_instance\\_test_log_config.json.\nValueError: '
1360+ # "Unable to configure handler 'access'\n[Errno 2] No such file "
1361+ # "or directory: "
1362+ # "'C:\\\\tracker\\\\path\\\\not_a_test_instance\\\\access.log'\n")
1363+ # sigh.....
1364+ output = re .sub ("directory: \' .*not_a" , 'directory: not_a' ,
1365+ cm .exception .args [0 ].replace (r'\\' ,'\\ ' ))
1366+ target = ("Error loading logging dict from "
1367+ "%s.\n "
1368+ "ValueError: Unable to configure handler 'access'\n "
1369+ "[Errno 2] No such file or directory: "
1370+ "%s'\n " % (log_config_filename , access_filename ))
1371+ self .assertEqual (output , target )
13571372
0 commit comments