/ tests / test_imports.py
test_imports.py
  1  """import検証テスト
  2  
  3  全モジュールがimportできること、DB/LLM系のimportが含まれていないことを確認する。
  4  """
  5  
  6  from __future__ import annotations
  7  
  8  import ast
  9  import importlib
 10  import pathlib
 11  from typing import Any
 12  
 13  import pytest
 14  
 15  # mureo-core パッケージルート
 16  _MUREO_ROOT = pathlib.Path(__file__).resolve().parent.parent / "mureo"
 17  
 18  # DB/LLM系の禁止importパターン
 19  _FORBIDDEN_MODULES: frozenset[str] = frozenset(
 20      {
 21          "sqlalchemy",
 22          "alembic",
 23          "asyncpg",
 24          "aiosqlite",
 25          "supabase",
 26          "openai",
 27          "anthropic",
 28          "google.generativeai",
 29          "langchain",
 30          "slack_bolt",
 31          "slack_sdk",
 32          "apscheduler",
 33          "fastapi",
 34          "uvicorn",
 35          "redis",
 36      }
 37  )
 38  
 39  # テスト対象モジュール一覧
 40  _ALL_MODULES: list[str] = [
 41      "mureo",
 42      "mureo.google_ads",
 43      "mureo.google_ads.client",
 44      "mureo.google_ads.mappers",
 45      "mureo.google_ads._ads",
 46      "mureo.google_ads._keywords",
 47      "mureo.google_ads._analysis",
 48      "mureo.google_ads._analysis_constants",
 49      "mureo.google_ads._analysis_performance",
 50      "mureo.google_ads._analysis_search_terms",
 51      "mureo.google_ads._analysis_keywords",
 52      "mureo.google_ads._analysis_budget",
 53      "mureo.google_ads._analysis_rsa",
 54      "mureo.google_ads._analysis_auction",
 55      "mureo.google_ads._analysis_btob",
 56      "mureo.google_ads._extensions",
 57      "mureo.google_ads._diagnostics",
 58      "mureo.google_ads._monitoring",
 59      "mureo.google_ads._creative",
 60      "mureo.google_ads._rsa_validator",
 61      "mureo.google_ads._rsa_insights",
 62      "mureo.google_ads._intent_classifier",
 63      "mureo.google_ads._message_match",
 64      "mureo.meta_ads",
 65      "mureo.meta_ads.client",
 66      "mureo.meta_ads.mappers",
 67      "mureo.meta_ads._campaigns",
 68      "mureo.meta_ads._ad_sets",
 69      "mureo.meta_ads._ads",
 70      "mureo.meta_ads._creatives",
 71      "mureo.meta_ads._audiences",
 72      "mureo.meta_ads._pixels",
 73      "mureo.meta_ads._insights",
 74      "mureo.meta_ads._analysis",
 75      "mureo.analysis",
 76      "mureo.analysis.lp_analyzer",
 77      "mureo.context",
 78      "mureo.context.errors",
 79      "mureo.context.models",
 80      "mureo.context.state",
 81      "mureo.context.strategy",
 82  ]
 83  
 84  
 85  def _collect_imports_from_file(filepath: pathlib.Path) -> list[str]:
 86      """ASTを使ってPythonファイルからimport文のモジュール名を収集する"""
 87      source = filepath.read_text(encoding="utf-8")
 88      tree = ast.parse(source, filename=str(filepath))
 89  
 90      imports: list[str] = []
 91      for node in ast.walk(tree):
 92          if isinstance(node, ast.Import):
 93              for alias in node.names:
 94                  imports.append(alias.name)
 95          elif isinstance(node, ast.ImportFrom):
 96              if node.module:
 97                  imports.append(node.module)
 98      return imports
 99  
100  
101  @pytest.mark.unit
102  class TestModuleImports:
103      """全モジュールがimportできることを確認"""
104  
105      @pytest.mark.parametrize("module_name", _ALL_MODULES)
106      def test_import_succeeds(self, module_name: str) -> None:
107          """各モジュールが正常にimportできる"""
108          mod = importlib.import_module(module_name)
109          assert mod is not None
110  
111  
112  @pytest.mark.unit
113  class TestNoForbiddenImports:
114      """DB/LLM系のimportが含まれていないことをAST解析で確認"""
115  
116      def _collect_all_py_files(self) -> list[pathlib.Path]:
117          """mureo/ 配下の全.pyファイルを収集"""
118          return sorted(_MUREO_ROOT.rglob("*.py"))
119  
120      def test_no_forbidden_imports_in_package(self) -> None:
121          """mureo/配下の全ファイルに禁止importが含まれていない"""
122          violations: list[str] = []
123  
124          for py_file in self._collect_all_py_files():
125              imports = _collect_imports_from_file(py_file)
126              for imp in imports:
127                  # トップレベルモジュール名で比較
128                  top_module = imp.split(".")[0]
129                  for forbidden in _FORBIDDEN_MODULES:
130                      if top_module == forbidden.split(".")[0] and imp.startswith(
131                          forbidden
132                      ):
133                          violations.append(
134                              f"{py_file.relative_to(_MUREO_ROOT.parent)}: "
135                              f"import {imp}"
136                          )
137  
138          assert (
139              violations == []
140          ), "禁止されたDB/LLM系のimportが検出されました:\n" + "\n".join(violations)