|
| 1 | +"""Tests for checkov/logging_init.py LOG_LEVEL handling. |
| 2 | +
|
| 3 | +Since logging_init.py executes at module import time, we test the core |
| 4 | +logic (basicConfig + setLevel with env-driven LOG_LEVEL) in isolation |
| 5 | +to verify that invalid values don't crash the process. |
| 6 | +""" |
| 7 | +import logging |
| 8 | +import os |
| 9 | +import subprocess |
| 10 | +import sys |
| 11 | +import unittest |
| 12 | + |
| 13 | +from checkov.logging_init import FALLBACK_LOG_LEVEL |
| 14 | + |
| 15 | + |
| 16 | +def _configure_logging(log_level_env: str | None = None) -> int | str: |
| 17 | + """Reproduce the initialization logic from checkov/logging_init.py. |
| 18 | +
|
| 19 | + Returns the effective LOG_LEVEL (str on success, int on fallback). |
| 20 | + """ |
| 21 | + raw = (log_level_env if log_level_env is not None |
| 22 | + else logging.getLevelName(FALLBACK_LOG_LEVEL)).upper() |
| 23 | + try: |
| 24 | + logging.basicConfig(level=raw, force=True) |
| 25 | + return raw |
| 26 | + except (ValueError, TypeError): |
| 27 | + logging.basicConfig(level=FALLBACK_LOG_LEVEL, force=True) |
| 28 | + return FALLBACK_LOG_LEVEL |
| 29 | + |
| 30 | + |
| 31 | +# --------------------------------------------------------------------------- |
| 32 | +# Valid Python log levels – these must all succeed |
| 33 | +# --------------------------------------------------------------------------- |
| 34 | +VALID_PYTHON_LEVELS = [ |
| 35 | + "DEBUG", |
| 36 | + "INFO", |
| 37 | + "WARNING", |
| 38 | + "ERROR", |
| 39 | + "CRITICAL", |
| 40 | + "NOTSET", |
| 41 | +] |
| 42 | + |
| 43 | +# --------------------------------------------------------------------------- |
| 44 | +# Valid Python aliases – accepted by logging but not the canonical names |
| 45 | +# --------------------------------------------------------------------------- |
| 46 | +VALID_PYTHON_ALIASES = [ |
| 47 | + "FATAL", # alias for CRITICAL |
| 48 | + "WARN", # alias for WARNING |
| 49 | +] |
| 50 | + |
| 51 | +# --------------------------------------------------------------------------- |
| 52 | +# Case-insensitive variants – .upper() should normalise these |
| 53 | +# --------------------------------------------------------------------------- |
| 54 | +CASE_VARIANTS = [ |
| 55 | + ("debug", "DEBUG"), |
| 56 | + ("info", "INFO"), |
| 57 | + ("Warning", "WARNING"), |
| 58 | + ("warning", "WARNING"), |
| 59 | + ("WaRnInG", "WARNING"), |
| 60 | + ("error", "ERROR"), |
| 61 | + ("Error", "ERROR"), |
| 62 | + ("critical", "CRITICAL"), |
| 63 | + ("CrItIcAl", "CRITICAL"), |
| 64 | + ("notset", "NOTSET"), |
| 65 | + ("fatal", "FATAL"), |
| 66 | + ("Warn", "WARN"), |
| 67 | +] |
| 68 | + |
| 69 | +# --------------------------------------------------------------------------- |
| 70 | +# Invalid / non-Python level strings – must fall back to WARNING |
| 71 | +# --------------------------------------------------------------------------- |
| 72 | +INVALID_LEVELS = [ |
| 73 | + # Common levels from other ecosystems (Java, Rust, syslog, etc.) |
| 74 | + "TRACE", |
| 75 | + "VERBOSE", |
| 76 | + "SEVERE", |
| 77 | + "FINE", |
| 78 | + "FINER", |
| 79 | + "FINEST", |
| 80 | + "OFF", |
| 81 | + "ALL", |
| 82 | + "NOTICE", |
| 83 | + "ALERT", |
| 84 | + "EMERG", |
| 85 | + "EMERGENCY", |
| 86 | + # Misspellings |
| 87 | + "DBUG", |
| 88 | + "DEUBG", |
| 89 | + "DUBUG", |
| 90 | + "INFOO", |
| 91 | + "WARINING", |
| 92 | + "WARNNING", |
| 93 | + "WARING", |
| 94 | + "EROR", |
| 95 | + "ERRROR", |
| 96 | + "CRTICAL", |
| 97 | + "CRITCAL", |
| 98 | + # Garbage |
| 99 | + "", |
| 100 | + " ", |
| 101 | + "123", |
| 102 | + "NONE", |
| 103 | + "NULL", |
| 104 | + "TRUE", |
| 105 | + "FALSE", |
| 106 | + "YES", |
| 107 | + "NO", |
| 108 | +] |
| 109 | + |
| 110 | +ALL_VALID_LEVELS = VALID_PYTHON_LEVELS + VALID_PYTHON_ALIASES |
| 111 | + |
| 112 | + |
| 113 | +class TestLoggingInitValidLevels(unittest.TestCase): |
| 114 | + """Verify every valid Python log level is accepted without error.""" |
| 115 | + |
| 116 | + def test_valid_levels(self) -> None: |
| 117 | + for level in VALID_PYTHON_LEVELS: |
| 118 | + with self.subTest(level=level): |
| 119 | + result = _configure_logging(level) |
| 120 | + self.assertEqual(result, level) |
| 121 | + |
| 122 | + def test_valid_aliases(self) -> None: |
| 123 | + for level in VALID_PYTHON_ALIASES: |
| 124 | + with self.subTest(level=level): |
| 125 | + result = _configure_logging(level) |
| 126 | + self.assertEqual(result, level) |
| 127 | + |
| 128 | + |
| 129 | +class TestLoggingInitCaseInsensitivity(unittest.TestCase): |
| 130 | + """LOG_LEVEL should be case-insensitive thanks to .upper().""" |
| 131 | + |
| 132 | + def test_case_variants(self) -> None: |
| 133 | + for raw, expected in CASE_VARIANTS: |
| 134 | + with self.subTest(raw=raw): |
| 135 | + result = _configure_logging(raw) |
| 136 | + self.assertEqual(result, expected) |
| 137 | + |
| 138 | + |
| 139 | +class TestLoggingInitInvalidLevels(unittest.TestCase): |
| 140 | + """Invalid LOG_LEVEL values must not crash; they should fall back.""" |
| 141 | + |
| 142 | + def test_invalid_levels_do_not_crash(self) -> None: |
| 143 | + for level in INVALID_LEVELS: |
| 144 | + with self.subTest(level=level): |
| 145 | + result = _configure_logging(level) |
| 146 | + self.assertEqual(result, FALLBACK_LOG_LEVEL, |
| 147 | + f"Expected fallback for invalid level {level!r}") |
| 148 | + |
| 149 | + def test_none_env_defaults_to_warning(self) -> None: |
| 150 | + result = _configure_logging(None) |
| 151 | + self.assertEqual(result, logging.getLevelName(FALLBACK_LOG_LEVEL)) |
| 152 | + |
| 153 | + |
| 154 | +class TestLoggingInitSetLevel(unittest.TestCase): |
| 155 | + """Verify setLevel behaviour with valid and invalid level strings.""" |
| 156 | + |
| 157 | + def test_setLevel_with_valid_levels(self) -> None: |
| 158 | + handler = logging.StreamHandler() |
| 159 | + for level in ALL_VALID_LEVELS: |
| 160 | + with self.subTest(level=level): |
| 161 | + handler.setLevel(level) # should not raise |
| 162 | + |
| 163 | + def test_setLevel_rejects_invalid_levels(self) -> None: |
| 164 | + """setLevel raises ValueError on invalid strings, confirming that |
| 165 | + the fallback in logging_init.py must reassign LOG_LEVEL so that |
| 166 | + downstream setLevel() calls don't crash. |
| 167 | + """ |
| 168 | + handler = logging.StreamHandler() |
| 169 | + for level in INVALID_LEVELS: |
| 170 | + if not level.strip(): |
| 171 | + continue # empty/whitespace handled differently |
| 172 | + with self.subTest(level=level): |
| 173 | + with self.assertRaises(ValueError, |
| 174 | + msg=f"setLevel({level!r}) should raise ValueError"): |
| 175 | + handler.setLevel(level.upper()) |
| 176 | + |
| 177 | + |
| 178 | +class TestLoggingInitModuleImport(unittest.TestCase): |
| 179 | + """End-to-end: verify the actual module import doesn't crash. |
| 180 | +
|
| 181 | + Uses subprocess so the module-level code runs fresh in a clean |
| 182 | + Python process. Skipped if checkov dependencies aren't installed. |
| 183 | + """ |
| 184 | + |
| 185 | + def _import_logging_init(self, log_level: str) -> subprocess.CompletedProcess: |
| 186 | + env = os.environ.copy() |
| 187 | + env["LOG_LEVEL"] = log_level |
| 188 | + return subprocess.run( |
| 189 | + [sys.executable, "-c", "import checkov.logging_init"], |
| 190 | + env=env, |
| 191 | + capture_output=True, |
| 192 | + text=True, |
| 193 | + timeout=30, |
| 194 | + ) |
| 195 | + |
| 196 | + def test_valid_levels_import_succeeds(self) -> None: |
| 197 | + for level in ALL_VALID_LEVELS: |
| 198 | + with self.subTest(level=level): |
| 199 | + result = self._import_logging_init(level) |
| 200 | + self.assertEqual(result.returncode, 0, |
| 201 | + f"Import crashed with LOG_LEVEL={level!r}:\n{result.stderr}") |
| 202 | + |
| 203 | + def test_invalid_levels_import_does_not_crash(self) -> None: |
| 204 | + """Invalid LOG_LEVEL values must not crash the import.""" |
| 205 | + for level in ["DBUG", "TRACE", "VERBOSE", "INFOO", "WARINING"]: |
| 206 | + with self.subTest(level=level): |
| 207 | + result = self._import_logging_init(level) |
| 208 | + self.assertEqual(result.returncode, 0, |
| 209 | + f"Import crashed with LOG_LEVEL={level!r}:\n{result.stderr}") |
| 210 | + |
| 211 | + def test_case_insensitive_import(self) -> None: |
| 212 | + for level in ["Warning", "debug", "error", "fatal", "Warn"]: |
| 213 | + with self.subTest(level=level): |
| 214 | + result = self._import_logging_init(level) |
| 215 | + self.assertEqual(result.returncode, 0, |
| 216 | + f"Import crashed with LOG_LEVEL={level!r}:\n{result.stderr}") |
| 217 | + |
| 218 | + |
| 219 | +if __name__ == "__main__": |
| 220 | + unittest.main() |
0 commit comments