Warnings fixed: - datetime.utcnow() → datetime.now(UTC) across 48+ files (src/ + tests/) - datetime.utcnow (callback ref) → lambda: datetime.now(UTC) in model fields (18 files) - Pydantic class Config → model_config = ConfigDict(...) (16 files) - Pydantic .dict() → .model_dump() (8 files) - ConfigDict(allow_population_by_field_name=True) → validate_by_name=True - SQLAlchemy declarative_base() import path updated - FastAPI on_event → lifespan context manager (app.py) - Import sorting (ruff I001) auto-fixed across all files - Fixed broken re-export chains that ruff F401 cleanup broke: _validate_bcp47: service.py now imports from dictionary_validation directly job_to_response: _job_routes.py and test imports from service_utils directly fetch_datasource_metadata: restored re-export in service.py - Added missing TranslateJobService import in _job_routes.py (was deleted by F401) - Added ConfigDict(protected_namespaces=()) for DashboardDatasetItem schema field - pytest.ini: replaced deprecated importmode with asyncio_mode All 440 tests pass with zero deprecation warnings.
116 lines
5.1 KiB
Python
116 lines
5.1 KiB
Python
# #region TestLoggingAuditFixes [C:3] [TYPE Module] [SEMANTICS test,logging,except,audit]
|
|
# @BRIEF Verify Class 2 fix — no bare `except: pass` patterns remain in src/.
|
|
# @RELATION BINDS_TO -> [EXT:frontend:PluginLoaderCore]
|
|
# @TEST_EDGE: except_pass_replaced -> bare `except: pass` patterns are absent from src/
|
|
# @TEST_EDGE: plugin_loader_uses_logger -> plugin_loader.py uses logger, not print
|
|
import ast
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
|
|
class TestLoggingAuditFixes:
|
|
"""Verify that all bare `except: pass` patterns have been replaced with proper logging."""
|
|
|
|
PLUGIN_LOADER_PATH = Path(__file__).parent.parent / "src" / "core" / "plugin_loader.py"
|
|
|
|
# #region _collect_src_files [C:1] [TYPE Function]
|
|
# @BRIEF Collect all .py files under src/ excluding test/ and mock/ directories.
|
|
@staticmethod
|
|
def _collect_src_files():
|
|
src_root = Path(__file__).parent.parent / "src"
|
|
return sorted(
|
|
p for p in src_root.rglob("*.py")
|
|
if "test" not in p.parts and "mock" not in p.parts and p.name != "__init__.py"
|
|
)
|
|
# #endregion _collect_src_files
|
|
|
|
# #region test_except_pass_replaced_with_log [C:2] [TYPE Function]
|
|
# @BRIEF AST audit: bare `except: pass` nodes are absent from src/ source files.
|
|
def test_except_pass_replaced_with_log(self):
|
|
files_with_bare_except_pass = []
|
|
|
|
for filepath in self._collect_src_files():
|
|
try:
|
|
tree = ast.parse(filepath.read_text(encoding="utf-8"), filename=str(filepath))
|
|
except SyntaxError:
|
|
continue
|
|
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.ExceptHandler):
|
|
continue
|
|
if node.type is not None:
|
|
continue
|
|
if len(node.body) == 1 and isinstance(node.body[0], ast.Pass):
|
|
files_with_bare_except_pass.append(str(filepath))
|
|
break
|
|
|
|
assert not files_with_bare_except_pass, (
|
|
f"Bare `except: pass` found in {len(files_with_bare_except_pass)} file(s):\n"
|
|
+ "\n".join(files_with_bare_except_pass)
|
|
)
|
|
# #endregion test_except_pass_replaced_with_log
|
|
|
|
# #region test_plugin_loader_uses_logger_not_print [C:2] [TYPE Function]
|
|
# @BRIEF plugin_loader.py (src/core/) uses logger for error/warning, not print().
|
|
def test_plugin_loader_uses_logger_not_print(self):
|
|
code = self.PLUGIN_LOADER_PATH.read_text(encoding="utf-8")
|
|
tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))
|
|
|
|
# Find all print() calls in executable code
|
|
print_lines = []
|
|
for node in ast.walk(tree):
|
|
if (isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Name)
|
|
and node.func.id == "print"):
|
|
print_lines.append(node.lineno)
|
|
|
|
assert len(print_lines) == 0, (
|
|
f"plugin_loader.py still contains print() calls at lines: {print_lines}"
|
|
)
|
|
# #endregion test_plugin_loader_uses_logger_not_print
|
|
|
|
# #region test_plugin_loader_imports_logger [C:2] [TYPE Function]
|
|
# @BRIEF plugin_loader.py imports a logger or _logger for structured reporting.
|
|
def test_plugin_loader_imports_logger(self):
|
|
code = self.PLUGIN_LOADER_PATH.read_text(encoding="utf-8")
|
|
tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))
|
|
|
|
has_logger_import = False
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
for alias in node.names:
|
|
if "logger" in alias.name.lower():
|
|
has_logger_import = True
|
|
break
|
|
|
|
assert has_logger_import, (
|
|
"plugin_loader.py should import 'logger' or '_logger' "
|
|
"for log-based error handling (instead of print)"
|
|
)
|
|
# #endregion test_plugin_loader_imports_logger
|
|
|
|
# #region test_plugin_loader_logger_calls_exist [C:2] [TYPE Function]
|
|
# @BRIEF plugin_loader.py references logger.error/warning/info in its code.
|
|
def test_plugin_loader_logger_calls_exist(self):
|
|
code = self.PLUGIN_LOADER_PATH.read_text(encoding="utf-8")
|
|
tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))
|
|
|
|
logger_calls = []
|
|
for node in ast.walk(tree):
|
|
if (isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Attribute)
|
|
and isinstance(node.func.value, ast.Name)
|
|
and "logger" in node.func.value.id.lower()):
|
|
logger_calls.append((node.lineno, node.func.attr))
|
|
|
|
error_levels = {"error", "warning", "info", "debug", "reason", "reflect", "explore"}
|
|
matching = [call for call in logger_calls if call[1] in error_levels]
|
|
assert len(matching) >= 4, (
|
|
f"plugin_loader.py has only {len(matching)} logger.error/warning/info calls "
|
|
f"(expected at least 4 for the 4 replaced print() statements): {matching}"
|
|
)
|
|
# #endregion test_plugin_loader_logger_calls_exist
|
|
# #endregion TestLoggingAuditFixes
|