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

1# core/diagnostics.py 

2""" 

3バグ報告時に添付できる診断情報バンドル(zip)を作る (C-1201)。 

4 

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 

15 

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 

19 

20logger = logging.getLogger(__name__) 

21 

22# バージョン情報を収集する対象の依存パッケージ 

23_DEPENDENCY_MODULES = ('PySide6', 'matplotlib', 'numpy', 'pandas', 'scipy', 'openpyxl', 'xlrd') 

24 

25 

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) 

43 

44 

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}") 

60 

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}") 

69 

70 return "\n".join(lines) 

71 

72 

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) 

78 

79 

80def build_diagnostic_bundle(out_path: str, settings_dict: dict[str, Any] | None = None) -> None: 

81 """ 

82 診断情報バンドル(zip)を out_path に書き出す。 

83 

84 含まれる内容: 

85 - environment.txt: アプリ/OS/Pythonバージョン、主要依存パッケージのバージョン 

86 - plugins.txt: 読み込み済みプラグインの一覧と成否 

87 - settings.txt: QSettingsの内容(呼び出し側がdictとして渡した場合のみ) 

88 - graphica.log (存在すれば): アプリのログファイル 

89 

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)) 

100 

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}")