Mercurial > p > roundup > code
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 |
