Coverage for graphica/core/diagnostics.py: 92.1%
58 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1# core/diagnostics.py
2"""
3バグ報告時に添付できる診断情報バンドル(zip)を作る (C-1201)。
5core/ の他モジュール同様GUI(PySide6)には依存しない: QSettingsの内容は
6呼び出し側(gui/mixins/help_mixin.py)が {key: value} のプレーンなdictに
7変換してから渡す。
8"""
9import logging
10import os
11import platform
12import sys
13import zipfile
14from datetime import datetime
16from graphica.core.app_paths import get_app_data_dir
17from graphica.core.version import APP_NAME, LOG_FILE_NAME, __version__
18from typing import Any
20logger = logging.getLogger(__name__)
22# バージョン情報を収集する対象の依存パッケージ
23_DEPENDENCY_MODULES = ('PySide6', 'matplotlib', 'numpy', 'pandas', 'scipy', 'openpyxl', 'xlrd')
26def _collect_environment_info() -> str:
27 lines = [
28 f"{APP_NAME} {__version__}",
29 f"生成日時: {datetime.now().astimezone().isoformat()}",
30 f"OS: {platform.platform()}",
31 f"Python: {sys.version}",
32 "",
33 "--- 依存パッケージ ---",
34 ]
35 for module_name in _DEPENDENCY_MODULES:
36 try:
37 module = __import__(module_name)
38 version = getattr(module, '__version__', '(バージョン不明)')
39 except Exception:
40 version = "未インストール、または読み込み失敗"
41 lines.append(f"{module_name}: {version}")
42 return "\n".join(lines)
45def _collect_plugin_info() -> str:
46 from graphica.core.plugin_api import get_loaded_plugin_records, get_plugin_registration_errors
47 records = get_loaded_plugin_records()
48 if records is None:
49 return "プラグインは未読み込みです。"
50 if not records: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 return "読み込まれたプラグインはありません。"
52 lines = []
53 for record in records:
54 name = record.get("name", "(不明)")
55 info = record.get("info") or {}
56 version = info.get("version", "?")
57 error = record.get("error")
58 status = "OK" if error is None else f"読み込み失敗: {error}"
59 lines.append(f"- {name} (v{version}): {status}")
61 # プラグイン全体としては読み込みに成功していても、個別のregister_xxx呼び出しが
62 # 失敗している場合はrecordのerrorには現れないため、別途一覧化する(フェーズA-2)。
63 registration_errors = get_plugin_registration_errors()
64 if registration_errors:
65 lines.append("")
66 lines.append("--- フック単位の登録失敗 ---")
67 for err in registration_errors:
68 lines.append(f"- [{err.plugin_name}] {err.hook_kind.value}: {err.message}")
70 return "\n".join(lines)
73def _collect_settings_info(settings_dict: dict[str, Any] | None) -> str:
74 if not settings_dict: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
75 return "(設定値なし)"
76 lines = [f"{key} = {value!r}" for key, value in sorted(settings_dict.items())]
77 return "\n".join(lines)
80def build_diagnostic_bundle(out_path: str, settings_dict: dict[str, Any] | None = None) -> None:
81 """
82 診断情報バンドル(zip)を out_path に書き出す。
84 含まれる内容:
85 - environment.txt: アプリ/OS/Pythonバージョン、主要依存パッケージのバージョン
86 - plugins.txt: 読み込み済みプラグインの一覧と成否
87 - settings.txt: QSettingsの内容(呼び出し側がdictとして渡した場合のみ)
88 - graphica.log (存在すれば): アプリのログファイル
90 Args:
91 out_path (str): 出力先のzipファイルパス。
92 settings_dict (dict | None): QSettingsの内容を {key: value} の
93 プレーンなdictに変換したもの。Noneならsettings.txtは省略する。
94 """
95 with zipfile.ZipFile(out_path, 'w', zipfile.ZIP_DEFLATED) as zf:
96 zf.writestr("environment.txt", _collect_environment_info())
97 zf.writestr("plugins.txt", _collect_plugin_info())
98 if settings_dict is not None:
99 zf.writestr("settings.txt", _collect_settings_info(settings_dict))
101 log_path = os.path.join(get_app_data_dir(), LOG_FILE_NAME)
102 if os.path.exists(log_path):
103 zf.write(log_path, arcname=LOG_FILE_NAME)
104 else:
105 zf.writestr("log_not_found.txt", f"ログファイルが見つかりませんでした: {log_path}")