comparison test/test_config.py @ 8428:cdf876bcd370

test: test dictLoggerConfig - working logging reset and windows Finally figured out why things weren't being restored. Bug in the code. Created a class fixture that stores and restores the logging config. Also using os.path.join and other machinations to make the tests run under windows and linux correctly.
author John Rouillard <rouilj@ieee.org>
date Wed, 20 Aug 2025 21:04:56 -0400
parents b34c3b8338f0
children 3210729950b1
comparison
equal deleted inserted replaced
8427:b34c3b8338f0 8428:cdf876bcd370
413 config._get_option('UMASK').set("xyzzy") 413 config._get_option('UMASK').set("xyzzy")
414 414
415 print(type(config._get_option('UMASK'))) 415 print(type(config._get_option('UMASK')))
416 416
417 417
418 @pytest.mark.usefixtures("save_restore_logging")
418 class TrackerConfig(unittest.TestCase): 419 class TrackerConfig(unittest.TestCase):
420
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)
419 482
420 backend = 'anydbm' 483 backend = 'anydbm'
421 484
422 def setUp(self): 485 def setUp(self):
423 self.dirname = '_test_instance' 486 self.dirname = '_test_instance'
1137 } 1200 }
1138 } 1201 }
1139 } 1202 }
1140 """) 1203 """)
1141 1204
1142 # save roundup logger state 1205 log_config_filename = os.path.join(self.instance.tracker_home,
1143 loggernames = ("", "roundup") 1206 "_test_log_config.json")
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"
1158 1207
1159 # happy path 1208 # happy path
1160 with open(log_config_filename, "w") as log_config_file: 1209 with open(log_config_filename, "w") as log_config_file:
1161 log_config_file.write(config1) 1210 log_config_file.write(config1)
1162 1211
1174 config = self.db.config.load_config_dict_from_json_file( 1223 config = self.db.config.load_config_dict_from_json_file(
1175 log_config_filename) 1224 log_config_filename)
1176 self.assertEqual( 1225 self.assertEqual(
1177 cm.exception.args[0], 1226 cm.exception.args[0],
1178 ('Error parsing json logging dict ' 1227 ('Error parsing json logging dict '
1179 '(_test_instance/_test_log_config.json) near \n\n ' 1228 '(%s) near \n\n '
1180 '"version": 1, # only supported version\n\nExpecting ' 1229 '"version": 1, # only supported version\n\nExpecting '
1181 'property name enclosed in double quotes: line 3 column 18.\n' 1230 '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)
1183 ) 1233 )
1184 1234
1185 # broken trailing , on last dict element 1235 # broken trailing , on last dict element
1186 test_config = config1.replace(' "ext://sys.stdout"', 1236 test_config = config1.replace(' "ext://sys.stdout"',
1187 ' "ext://sys.stdout",' 1237 ' "ext://sys.stdout",'
1196 # FIXME check/remove when 3.13. is min supported version 1246 # FIXME check/remove when 3.13. is min supported version
1197 if "property name" in cm.exception.args[0]: 1247 if "property name" in cm.exception.args[0]:
1198 self.assertEqual( 1248 self.assertEqual(
1199 cm.exception.args[0], 1249 cm.exception.args[0],
1200 ('Error parsing json logging dict ' 1250 ('Error parsing json logging dict '
1201 '(_test_instance/_test_log_config.json) near \n\n' 1251 '(%s) near \n\n'
1202 ' }\n\nExpecting property name enclosed in double ' 1252 ' }\n\n'
1203 'quotes: line 37 column 6.') 1253 'Expecting property name enclosed in double '
1254 'quotes: line 37 column 6.' % log_config_filename)
1204 ) 1255 )
1205 1256
1206 # 3.13+ diags FIXME 1257 # 3.13+ diags FIXME
1207 print('FINDME') 1258 print('FINDME')
1208 print(cm.exception.args[0]) 1259 print(cm.exception.args[0])
1209 _junk = ''' 1260 _junk = '''
1210 if "property name" not in cm.exception.args[0]: 1261 if "property name" not in cm.exception.args[0]:
1211 self.assertEqual( 1262 self.assertEqual(
1212 cm.exception.args[0], 1263 cm.exception.args[0],
1213 ('Error parsing json logging dict ' 1264 ('Error parsing json logging dict '
1214 '(_test_instance/_test_log_config.json) near \n\n' 1265 '(%s) near \n\n'
1215 ' }\n\nExpecting property name enclosed in double ' 1266 ' "stream": "ext://sys.stdout"\n\n'
1216 'quotes: line 37 column 6.') 1267 'Expecting property name enclosed in double '
1268 'quotes: line 37 column 6.' % log_config_filename)
1217 ) 1269 )
1218 ''' 1270 '''
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
1230 # happy path for init_logging() 1271 # happy path for init_logging()
1231 1272
1232 # verify preconditions 1273 # verify preconditions
1233 logger = logging.getLogger("roundup") 1274 logger = logging.getLogger("roundup")
1234 self.assertEqual(logger.level, 40) # error default from config.ini 1275 self.assertEqual(logger.level, 40) # error default from config.ini
1267 # FIXME: remove mangle after 3.12 min version 1308 # FIXME: remove mangle after 3.12 min version
1268 self.assertEqual( 1309 self.assertEqual(
1269 cm.exception.args[0].replace( 1310 cm.exception.args[0].replace(
1270 "object\n", "object, got 'int'\n"), 1311 "object\n", "object, got 'int'\n"),
1271 ('Error loading logging dict from ' 1312 ('Error loading logging dict from '
1272 '_test_instance/_test_log_config.json.\n' 1313 '%s.\n'
1273 "ValueError: Unable to configure formatter 'http'\n" 1314 "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)
1275 ) 1317 )
1276 1318
1277 # broken invalid level MANGO 1319 # broken invalid level MANGO
1278 test_config = config1.replace( 1320 test_config = config1.replace(
1279 ': "INFO", # can', 1321 ': "INFO", # can',
1286 with self.assertRaises(configuration.LoggingConfigError) as cm: 1328 with self.assertRaises(configuration.LoggingConfigError) as cm:
1287 config = self.db.config.init_logging() 1329 config = self.db.config.init_logging()
1288 self.assertEqual( 1330 self.assertEqual(
1289 cm.exception.args[0], 1331 cm.exception.args[0],
1290 ("Error loading logging dict from " 1332 ("Error loading logging dict from "
1291 "_test_instance/_test_log_config.json.\nValueError: " 1333 "%s.\nValueError: "
1292 "Unable to configure logger 'roundup.hyperdb'\nUnknown level: " 1334 "Unable to configure logger 'roundup.hyperdb'\nUnknown level: "
1293 "'MANGO'\n") 1335 "'MANGO'\n" % log_config_filename)
1294 1336
1295 ) 1337 )
1296 1338
1297 # broken invalid output directory 1339 # broken invalid output directory
1298 test_config = config1.replace( 1340 test_config = config1.replace(
1299 ' "_test_instance/access.log"', 1341 ' "_test_instance/access.log"',
1300 ' "not_a_test_instance/access.log"') 1342 ' "not_a_test_instance/access.log"')
1343 access_filename = os.path.join("not_a_test_instance", "access.log")
1344
1301 with open(log_config_filename, "w") as log_config_file: 1345 with open(log_config_filename, "w") as log_config_file:
1302 log_config_file.write(test_config) 1346 log_config_file.write(test_config)
1303 1347
1304 # file is made relative to tracker dir. 1348 # file is made relative to tracker dir.
1305 self.db.config["LOGGING_CONFIG"] = '_test_log_config.json' 1349 self.db.config["LOGGING_CONFIG"] = '_test_log_config.json'
1306 with self.assertRaises(configuration.LoggingConfigError) as cm: 1350 with self.assertRaises(configuration.LoggingConfigError) as cm:
1307 config = self.db.config.init_logging() 1351 config = self.db.config.init_logging()
1308 1352
1309 # error includes full path which is different on different 1353 # error includes full path which is different on different
1310 # CI and dev platforms. So munge the path using re.sub. 1354 # CI and dev platforms. So munge the path using re.sub and
1311 self.assertEqual( 1355 # replace. Windows needs replace as the full path for windows
1312 re.sub("directory: \'/.*not_a", 'directory: not_a' , 1356 # to the file has '\\\\' not '\\' when taken from __context__.
1313 cm.exception.args[0]), 1357 # E.G.
1314 ("Error loading logging dict from " 1358 # ("Error loading logging dict from '
1315 "_test_instance/_test_log_config.json.\n" 1359 # '_test_instance\\_test_log_config.json.\nValueError: '
1316 "ValueError: Unable to configure handler 'access'\n" 1360 # "Unable to configure handler 'access'\n[Errno 2] No such file "
1317 "[Errno 2] No such file or directory: " 1361 # "or directory: "
1318 "not_a_test_instance/access.log'\n" 1362 # "'C:\\\\tracker\\\\path\\\\not_a_test_instance\\\\access.log'\n")
1319 ) 1363 # sigh.....
1320 ) 1364 output = re.sub("directory: \'.*not_a", 'directory: not_a' ,
1321 1365 cm.exception.args[0].replace(r'\\','\\'))
1322 ''' 1366 target = ("Error loading logging dict from "
1323 # rip down all the loggers leaving the root logger reporting 1367 "%s.\n"
1324 # to stdout. 1368 "ValueError: Unable to configure handler 'access'\n"
1325 # otherwise logger config is leaking to other tests 1369 "[Errno 2] No such file or directory: "
1326 1370 "%s'\n" % (log_config_filename, access_filename))
1327 roundup_loggers = [logging.getLogger(name) for name in 1371 self.assertEqual(output, target)
1328 logging.root.manager.loggerDict 1372
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)
1357

Roundup Issue Tracker: http://roundup-tracker.org/