さて、今回は外部プログラムを実行するプログラムの仕様概要を考察していきます。
最近はAIに概略を決めさせられるので楽です。
大体、必要なライブラリをGeminiに挙げさせつつ、流れを決めました。
(正誤は現状不明)
1.動的ライブラリのロード:
今回のテスト対象はC#とpythonで考える。
.csや.DLLや.py、Pythonライブラリなどを動的に読み込んでテストすると想定する。
C#ライブラリのロード:
ctypes (Python): C言語で記述された動的ライブラリ(.dll、.soなど)をロードし、関数を呼び出す。
ctypes.CDLL()、ctypes.WinDLL(): 動的ライブラリをロード。
clr (pythonnet): .NETアセンブリ(.dll)をロードし、.NETオブジェクトを操作。
clr.AddReference(): .NETアセンブリを参照に追加。
clr.System.Reflection.Assembly.LoadWithPartialName(): アセンブリをロード。
Pythonライブラリのロード:
importlib.import_module(): モジュールをロード。
pkgutil (Python): パッケージ内のモジュールを列挙、ロード。
2. 外部プログラムの関数情報、引数情報取得:
読み込んだオブジェクトから情報を得る。
inspect (Python): Pythonオブジェクトの情報を取得。
ast (Python): Pythonコードの抽象構文木(AST)を操作。
pefile (Python): Windows PEファイル(DLLなど)の情報を取得。
pythonnet (Python): .NET DLLの情報を取得。
3.テスト用の引数をテキトーに作る
この辺は独自に作るので色々試行錯誤が必要になる。
4.C#の引数はPythonからC#に型変換
既にテストプログラムを作って試したのですが、C#のジェネリック型とかPythonでは使えない型はたくさんある。
前述のclrで.Netの型を確認して、変換で使えるようなら変換、使えないならエラーを出す必要がある。
この辺は試行錯誤が必要。
clr.AddReference("System")
import System
from System import Object,String
from System.Collections.Generic import Dictionary as NetDictionary
from System.Collections.Generic import List as NetList
5.外部プログラムの実行
subprocess (Python): 外部プログラムの実行、結果の取得。
subprocess.run(): プログラムを実行し、結果を CompletedProcess オブジェクトで返す。
subprocess.Popen(): プログラムを非同期で実行し、入出力ストリームを操作。
os (Python): ファイルパスの操作、環境変数の取得。
os.path: ファイルパスの操作(存在確認、拡張子取得など)。
os.environ: 環境変数の取得、設定。
sh (Python): 外部プログラムをPythonの関数のように呼び出す。
clr (pythonnet): .NET DLLの関数を実行する。
6.C#の戻り値はPythonの型に変換
戻り値でも引き数の時のように、変換が必要な型が有る
7.結果と予測値を比較。OKなら次、NGならUser確認
現状はNGなら実際の結果で予測値の設定を変更するかUser確認で進める。
全て変更する/しないの選択肢も欲しいかも。
試作
そして、下記は試作中のテストプログラムです。
一応、すごく単純な関数ならテストできるかもしれませんが、これをベースに機能拡張、不具合予定です。
もしも、本機能に興味、アドバイスが有ればコメントをお願いします
なお、前回では関数実行までしか考えていませんでしたが、C#を動かすにあたってクラスとメソッドも動かす仕様に拡張しています。
この企画の目的は私の理想のプログラムテストの形を探って行く事ですので、今後も仕様はガンガン変えていきます。ご承知おきを。
※ちょこちょこと独自の載せてない関数も有りますが、多分ChatGPTで補完できる程度の小さな関数です。
※数点不具合が見つかってますので、後に修正Versionの記事を載せる予定です。
・execute_class_test_caseに参照での書込みに関わるエラーあり。
chck_resultで実際の結果でexpectedの設定を変えた時に次のtestまで値が書き換わる。
import ast
import inspect
import random
import json
import importlib.util
import os
import JSON_Control
from typing import List, Any
import ListControl
import DLLControl
import clr
import iterto
def get_file_type(file_path: str) -> str:
"""ファイルパスからファイルタイプを判別する純粋関数"""
_, ext = os.path.splitext(file_path)
return ext.lower()
def load_python_file(file_path: str) -> ast.Module:
"""Pythonファイルをロードする純粋関数"""
with open(file_path, 'r', encoding='utf-8') as file:
return ast.parse(file.read())
def get_function_info(file_path: str, options: dict) -> list:
file_type = get_file_type(file_path)
"""ロードされたファイルから関数情報を取得する純粋関数"""
function_informations = []
if file_type == '.cs':
file_name, ext = os.path.splitext(file_path)
dll_path = file_name + ".dll"
references = options.get("references")
DLLControl.compile_source_to_dll(file_path, dll_path, references=references)
# DLL側のメソッド情報を取得
function_informations = DLLControl.get_dll_function_info(dll_path)
program_path = dll_path
elif file_type == '.dll':
function_informations = DLLControl.get_dll_function_info(file_path)
program_path = file_path
elif file_type == '.py':
loaded_file = load_python_file(file_path)
# Pythonの関数情報を取得
function_informations = get_python_function_info(loaded_file)
program_path = file_path
else:
raise ValueError(f"Unsupported file type: {file_type}")
for function_information in function_informations:
function_information["program_path"] = program_path
return function_informations
def get_python_function_info(module: ast.Module) -> list:
"""Pythonファイルから関数情報を取得する純粋関数"""
function_infos = []
for node in ast.walk(module):
if isinstance(node, ast.FunctionDef):
# クラス内の __init__ はクラスの初期化情報として別途扱うため除外
if node.name == "__init__":
continue
function_name = node.name
arguments = [arg.arg for arg in node.args.args]
function_infos.append({
"function_name": function_name,
"class_name": "", # 関数の場合は空文字列
"argument_names": arguments,
"argument_types": [None] * len(arguments) # Pythonでは型ヒントがない場合、Noneとする
})
return function_infos
def get_python_class_init_info(module: ast.Module) -> list:
"""
Pythonファイルからクラス定義を探し、コンストラクタ(__init__)の引数情報を取得する。
クラスの初期化テストとして、テスト設定に "type" を "InstantiateClass" として追加する。
"""
constructor_infos = []
for node in ast.walk(module):
if isinstance(node, ast.ClassDef):
class_name = node.name
# クラス内から __init__ を探す
init_args = []
init_types = []
for item in node.body:
if isinstance(item, ast.FunctionDef) and item.name == "__init__":
# 第一引数 self を除く
init_args = [arg.arg for arg in item.args.args[1:]]
init_types = [None] * len(init_args)
break
# コンストラクタ情報をテストケースとして追加(存在しなくても、デフォルトコンストラクタのテスト)
constructor_infos.append({
"function_name": "__init__",
"class_name": class_name,
"argument_names": init_args,
"argument_types": init_types,
"is_constructor": True # フラグでコンストラクタであることを明示
})
return constructor_infos
def infer_argument_type(argument_name, type_inference_rules):
"""引数の名前から型を推測する"""
lower_name = argument_name.lower()
if any(keyword in lower_name for keyword in type_inference_rules["numeric_keywords"]):
return "numeric"
elif any(keyword in lower_name for keyword in type_inference_rules["string_keywords"]):
return "string"
elif any(keyword in lower_name for keyword in type_inference_rules["list_keywords"]):
return "list"
elif any(keyword in lower_name for keyword in type_inference_rules["dictionary_keywords"]):
return "dictionary"
else:
return "unknown"
def generate_test_cases(function_informations, type_inference_rules):
"""テストケースを生成する(同値分割、境界値分析、ペアワイズテスト)
・function_informationに "methods" キーが存在する場合はクラス情報として扱い、
初期化(コンストラクタ)の引数と各メソッドのテスト設定をひとまとめにする。
・それ以外はグローバルな関数として扱う。
"""
test_cases = []
for info in function_informations:
program_path = info.get("program_path", "")
# クラス情報の場合("methods"キーが存在する場合)
if "methods" in info:
class_name = info.get("class_name", "")
# コンストラクタの引数情報
init_arg_names = info.get("init_argument_names", [])
init_arg_types = info.get("init_argument_types", [])
# 推論処理:不足分は型推論で補完
init_types = []
for i, arg in enumerate(init_arg_names):
if i < len(init_arg_types) and init_arg_types[i]:
init_types.append(init_arg_types[i])
else:
init_types.append(infer_argument_type(arg, type_inference_rules))
# コンストラクタ引数のテスト値の組み合わせを生成
init_arg_values_sets = [generate_argument_values(arg_type) for arg_type in init_types]
if len(init_arg_values_sets) == 0:
init_combinations = [[]]
else:
# 全組み合わせの直積を生成
init_combinations = list(itertools.product(*init_arg_values_sets))
# 各メソッドについてテストケースを生成
methods_info = info.get("methods", [])
# 各メソッドごとに、テスト値の組み合わせからテストオブジェクトのリストを作成
method_tests_lists = []
for method in methods_info:
method_name = method.get("method_name", "")
m_arg_names = method.get("argument_names", [])
m_arg_types = method.get("argument_types", [])
# 推論処理
m_types = []
for i, arg in enumerate(m_arg_names):
if i < len(m_arg_types) and m_arg_types[i]:
m_types.append(m_arg_types[i])
else:
m_types.append(infer_argument_type(arg, type_inference_rules))
# テスト値の組み合わせを生成
m_arg_values_sets = [generate_argument_values(arg_type) for arg_type in m_types]
if len(m_arg_values_sets) == 0:
m_combinations = [[]]
else:
m_combinations = list(itertools.product(*m_arg_values_sets))
# 各組み合わせごとにメソッドテスト設定を作成
method_tests = []
for comb in m_combinations:
method_tests.append({
"method_name": method_name,
"argument_names": m_arg_names,
"argument_types": m_types,
"arguments": list(comb),
"check_result": {"expected": ""}
})
method_tests_lists.append(method_tests)
# クラスのテストケースは、初期化引数の各組み合わせと、各メソッドのテスト設定の直積により生成
for init_comb in init_combinations:
# 各メソッドのテストケースの直積(全メソッドに対して1組み合わせずつ選ぶ)
for methods_comb in itertools.product(*method_tests_lists):
test_case = {
"type": "ExecuteProgram ",
"settings": {
"program_path": program_path,
"class_name": class_name,
"argument_names": init_arg_names,
"argument_types": init_types,
"arguments": list(init_comb),
"methods": list(methods_comb)
}
}
test_cases.append(test_case)
else:
# グローバルな関数の場合の処理
class_name = info.get("class_name", "")
function_name = info.get("function_name", "")
argument_names = info.get("argument_names", [])
arguments_types = info.get("argument_types", None)
arg_types = []
if arguments_types:
for i, arg in enumerate(argument_names):
if i < len(arguments_types) and arguments_types[i]:
arg_types.append(arguments_types[i])
else:
arg_types.append(infer_argument_type(arg, type_inference_rules))
else:
arg_types = [infer_argument_type(arg, type_inference_rules) for arg in argument_names]
arg_values_sets = [generate_argument_values(arg_type) for arg_type in arg_types]
if len(arg_values_sets) == 0:
combinations = [[]]
else:
combinations = list(itertools.product(*arg_values_sets))
for arg_values in combinations:
test_case = {
"type": "ExecuteProgram",
"settings": {
"program_path": program_path,
"function_name": function_name,
"class_name": class_name,
"argument_names": argument_names,
"argument_types": arg_types,
"arguments": list(arg_values),
"check_result": {"expected": ""}
}
}
test_cases.append(test_case)
return test_cases
def generate_argument_values(arg_type):
"""引数の型に基づいて代表的な値を生成する(同値分割、境界値分析)"""
if arg_type == "numeric":
return [1, -1, 0, 2147483647, -2147483647]
elif arg_type == "string":
return ["", "test", "a" * 10, None]
elif arg_type == "list":
return [[], [1]]
elif arg_type == "dictionary":
return [{}, {"key": "value"}]
elif arg_type == "int":
return [1, -1, 0, 2147483647, -2147483648]
elif arg_type == "double":
return [1.0, -1.0, 0.0, 3.14, float('inf'), float('-inf'), float('nan')]
elif arg_type == "bool":
return [True, False]
elif arg_type == "DateTime":
return ["2023-10-27T10:00:00", "1970-01-01T00:00:00", None]
elif arg_type == "String":
return ["", "test", "a" * 1024, None]
elif arg_type == "String[]":
return [[""], ["test"], ["test1", "test2"], None]
elif arg_type == "List<string>":
return [[""], ["test"], ["test1", "test2"], None]
elif arg_type in ("Dictionary<string,string>", "Dictionary`2"):
return [ {"key": "value"}, {},None]
elif arg_type == "Object":
return [{"key": "value"}, None]
else:
return [None]
def pairwise_combinations(lists):
"""ペアワイズの組み合わせを生成する"""
if not lists:
yield []
elif len(lists) == 1:
for item in lists[0]:
yield [item]
else:
for pair in itertools.product(lists[0], lists[1]):
rest_lists = lists[2:]
if not rest_lists:
yield list(pair)
else:
for rest in generate_combinations(rest_lists):
yield list(pair) + rest
def generate_combinations(lists):
"""リストの組み合わせを生成する"""
if not lists:
yield []
else:
for item in lists[0]:
for rest in generate_combinations(lists[1:]):
yield [item] + rest
# --- グローバル関数用テスト実行 ---
def execute_function_test_case(settings):
program_path = settings.get("program_path", "")
function_name = settings.get("function_name", "")
class_name = settings.get("class_name", "")
argument_values = settings.get("arguments", [])
argument_names = settings.get("argument_names", [])
argument_types = settings.get("argument_types", [])
try:
if program_path.lower().endswith(".dll"):
# C#のDLLの場合
assembly = DLLControl.load_dll(os.path.abspath(program_path))
target_type = assembly.GetType(class_name)
if target_type:
method = target_type.GetMethod(function_name)
if method:
converted_args = [DLLControl.convert_python_to_cs(val, typ) for val, typ in zip(argument_values, argument_types)]
format_str = ",".join(ListControl.format_merge_multiple_list(
"{list3} {list1} = {list2}", "",
list1=argument_names, list2=converted_args, list3=argument_types))
print(f"{class_name}().{function_name}({format_str})")
if method.IsStatic:
result = method.Invoke(None, converted_args)
else:
# インスタンス生成(デフォルトコンストラクタ)してメソッド実行
instance = DLLControl.create_instance(assembly, class_name, *[])
result = instance.GetType().GetMethod(function_name).Invoke(instance, converted_args)
else:
raise AttributeError(f"メソッドが見つかりません: {function_name}")
else:
raise AttributeError(f"クラスが見つかりません: {class_name}")
else:
# Pythonファイルの場合
spec = importlib.util.spec_from_file_location(function_name, program_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
function = getattr(module, function_name)
argument_names = get_argument_names(function)
format_str = ",".join(ListControl.format_merge_multiple_list(
"{list1} = {list2}", "",
list1=argument_names, list2=argument_values))
print(f"Function: {function_name}({format_str})")
result = function(*argument_values)
settings = check_result(settings,result)
except FileNotFoundError:
print(f"ファイルが見つかりません: {program_path}")
except AttributeError as e:
print(f"クラスのインスタンス化またはメソッド呼び出しに失敗しました: {e}")
except Exception as e:
print(f"エラーが発生しました: {e}")
# --- クラステスト用:インスタンス生成+各メソッド実行 ---
def execute_class_test_case(settings):
program_path = settings.get("program_path", "")
class_name = settings.get("class_name", "")
init_argument_names = settings.get("argument_names", [])
init_argument_types = settings.get("argument_types", [])
init_argument_values = settings.get("arguments", [])
methods = settings.get("methods", [])
try:
if program_path.lower().endswith(".dll"):
# C#のDLLの場合
assembly = DLLControl.load_dll(os.path.abspath(program_path))
target_type = assembly.GetType(class_name)
if not target_type:
raise AttributeError(f"クラスが見つかりません: {class_name}")
converted_init_args = [DLLControl.convert_python_to_cs(val, typ) for val, typ in zip(init_argument_values, init_argument_types)]
instance = DLLControl.create_instance(assembly, class_name, *converted_init_args)
class_fmt = ",".join(ListControl.format_merge_multiple_list("{list3} {list1}={list2}", "", list1=init_argument_names, list2=converted_init_args,list3 = init_argument_types))
# 各メソッド実行
for method_setting in methods:
method_name = method_setting.get("method_name", "")
m_argument_names = method_setting.get("argument_names", [])
m_argument_types = method_setting.get("argument_types", [])
m_argument_values = method_setting.get("arguments", [])
fmt = ",".join(ListControl.format_merge_multiple_list("{list3} {list1}={list2}", "", list1=m_argument_names, list2=m_argument_values,list3 = m_argument_types))
converted_m_args = [DLLControl.convert_python_to_cs(val, typ) for val, typ in zip(m_argument_values, m_argument_types)]
print(f"{class_name}({class_fmt}).{method_name}({fmt})")
method = instance.GetType().GetMethod(method_name)
if not method:
raise AttributeError(f"メソッドが見つかりません: {method_name}")
cs_result = method.Invoke(instance, converted_m_args)
result = DLLControl.convert_cs_to_python(cs_result)
method_setting = check_result(method_setting,result)
else:
# Pythonクラスの場合
spec = importlib.util.spec_from_file_location(class_name, program_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
target = getattr(module, class_name)
if not inspect.isclass(target):
raise AttributeError(f"{class_name} is not a class")
instance = target(*init_argument_values)
print(f"{class_name} インスタンス生成: 引数 {list(zip(init_argument_names, init_argument_values))}")
# 各メソッド実行
for method_setting in methods:
method_name = method_setting.get("method_name", "")
m_argument_names = method_setting.get("argument_names", [])
m_argument_types = method_setting.get("argument_types", [])
m_argument_values = method_setting.get("arguments", [])
fmt = ",".join(ListControl.format_merge_multiple_list("{list1}={list2}", "", list1=m_argument_names, list2=m_argument_values))
print(f"{class_name}.{method_name}({fmt})")
if not hasattr(instance, method_name):
raise AttributeError(f"メソッドが見つかりません: {method_name}")
method = getattr(instance, method_name)
result = method(*m_argument_values)
method_setting = check_result(method_setting,result)
settings["methods"] = methods
return settings
except FileNotFoundError:
print(f"ファイルが見つかりません: {program_path}")
except AttributeError as e:
print(f"クラスのインスタンス生成またはメソッド呼び出しに失敗しました: {e}")
except Exception as e:
print(f"エラーが発生しました: {e}")
def check_result(check_setting,result):
check_result = check_setting.get("check_result", {})
if check_result:
expected = check_result.get("expected")
if result == expected:
print(f"OK Result: {result}")
else:
print(f"NG Result: {result}, expected: {expected}")
user_input = input("OverWrite the expected result setting with the actual results? :(W) ")
if user_input.upper() == "W":
check_result["expected"] = result
check_setting["check_result"] = check_result
return check_setting
# --- テストケース全体の実行 ---
def execute_program(test_cases):
"""テストケースに基づいてプログラムを実行する"""
for test_case in test_cases:
settings = test_case.get("settings", {})
methods = settings.get("methods", "")
if methods:
settings = execute_class_test_case(settings)
else:
settings = execute_function_test_case(settings)
test_case["settings"]=settings
return test_cases
def write_test_cases(program_path, output_file_path, test_data_name, type_inference_rules, options):
function_informations = get_function_info(program_path, options)
if not function_informations:
return
test_cases = generate_test_cases(function_informations, type_inference_rules)
test_plan_list = {test_data_name: test_cases}
# JSONファイルに出力
JSON_Control.WriteDictionary(output_file_path, test_plan_list)
return test_plan_list
def get_argument_names(func: Any) -> list:
"""
定義された引数名を取得する関数
"""
sig = inspect.signature(func)
arg_names = list(sig.parameters.keys())
return arg_names
def normalize_program_output(output):
"""
外部プログラムの戻り値 output を解析し、以下のキーを持つ辞書を返す:
- "success": 実行の成否(True/False)
- "result_value": 得られた結果または判断理由
- "error": エラー情報(あれば)
"""
if isinstance(output, dict):
result = output.get("result_value", None)
error = output.get("error", None)
success_judge = False if error else True
if result:
result_dictionary = {
"success": output.get("success", success_judge),
"result_value": result,
}
result_dictionary.update(output)
return result_dictionary
else:
if error:
return {"success": output.get("success", success_judge), "result_value": output, "error": error}
else:
return {"success": output.get("success", success_judge), "result_value": output}
elif isinstance(output, (tuple, list)):
return {"success": True, "result_value": output}
elif isinstance(output, bool):
return {"success": True, "result_value": output, "error": None}
elif isinstance(output, str):
return {"success": True, "result_value": output, "error": None}
else:
return {"success": True, "result_value": output, "error": None}
def main():
type_inference_rules = {
"numeric_keywords": {'size', 'count', 'width', 'height', 'ratio', 'length', 'number', 'index'},
"string_keywords": {'name', 'text', 'message', 'title', 'path', 'filename'},
"list_keywords": {"list"},
"dictionary_keywords": {"dict", "dictionary"}
}
options = {"references": [".\\Reference\\Newtonsoft.Json.13.0.3\\lib\\net35\\Newtonsoft.Json.dll"]}
# C# ソース(.cs)のテスト
program_path = "../../../Tools/diff.cs"
output_filename = "Test_cs.json"
data_name = "test"
test_cases = write_test_cases(program_path, output_filename, data_name, type_inference_rules, options)
if test_cases:
execute_program(test_cases[data_name])
JSON_Control.WriteDictionary(output_filename, test_cases)
# DLL のテスト
program_path = "../../../Tools/diff.dll"
output_filename = "Test_dll.json"
data_name = "test_dll"
test_cases = write_test_cases(program_path, output_filename, data_name, type_inference_rules, options)
if test_cases:
test_cases[data_name] = execute_program(test_cases[data_name])
JSON_Control.WriteDictionary(output_filename, test_cases)
# Python ファイルのテスト(クラスの初期化やメソッドのテストを含む)
program_path = "./Sources/Common/Text.py"
output_filename = "test_config.json"
data_name = "test"
test_cases = write_test_cases(program_path, output_filename, data_name, type_inference_rules, options)
if test_cases:
test_cases[data_name] = execute_program(test_cases[data_name])
JSON_Control.WriteDictionary(output_filename, test_cases)
if __name__ == "__main__":
main()
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import os
import pefile
from typing import Dict, Any, List, Optional
import subprocess
import ctypes
import clr
clr.AddReference("System.Reflection")
from System.Reflection import Assembly
clr.AddReference("System")
import System
from System import Object,String
from System.Collections.Generic import Dictionary as NetDictionary
from System.Collections.Generic import List as NetList
def get_default_csc_path() -> str:
"""
デフォルトの CSC のパスを返す。
デフォルトは %SystemRoot%\Microsoft.NET\Framework\v4.0.30319\csc.exe 。
"""
system_root = os.environ.get("SystemRoot", "C:\\Windows")
return os.path.join(system_root, "Microsoft.NET", "Framework", "v4.0.30319", "csc.exe")
def compile_source_to_dll(cs_file: str, output_dll: str="test.dll", build_method: str = "csc",
csc_path: str = None, references: list = None) -> bool:
"""
指定された C# ソースファイルを DLL 化する関数。
build_method が "csc" の場合、CSC の実行ファイル(直接パス指定)を用いてコンパイルする。
build_method が "msbuild" の場合は、csproj ファイルなどを想定して MSBuild を使用する(簡易実装)。
追加の参照 DLL は references リストで指定する。
"""
if build_method == "csc":
if csc_path is None:
csc_path = get_default_csc_path()
compile_command = [csc_path, "/target:library", f"/out:{os.path.abspath(output_dll)}", os.path.abspath(cs_file)]
if references:
for ref in references:
compile_command.append(f'/reference:"{ref}"')
elif build_method == "msbuild":
compile_command = ["msbuild", cs_file]
else:
raise ValueError(f"Unsupported build method: {build_method}")
print("Compiling C# source with command:", " ".join(compile_command))
try:
result = subprocess.run(compile_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=False, text=True)
except FileNotFoundError as e:
print("Build tool not found:", e)
return False
if result.returncode != 0:
print("Compilation failed:")
print(result.stdout)
print(result.stderr)
return False
if not os.path.exists(output_dll):
print(f"Compilation succeeded but {output_dll} not found.")
return False
print(f"Compilation succeeded: {output_dll} created.")
return True
def load_native_dll(dll_path: str) -> object:
"""ネイティブDLLをロードする純粋関数"""
import ctypes
abs_dll_path = os.path.abspath(dll_path)
try:
return ctypes.WinDLL(abs_dll_path)
except OSError as e:
raise OSError(f"Failed to load native DLL {dll_path}: {e}")
def is_native_dll(file_path: str) -> bool:
"""DLLがネイティブかマネージドかを判別する純粋関数"""
try:
pe = pefile.PE(file_path)
return hasattr(pe, 'DIRECTORY_ENTRY_EXPORT')
except pefile.PEFormatError:
return False
def load_dll(file_path):
if is_native_dll(file_path):
return load_native_dll(file_path)
else:
return load_managed_dll(file_path)
def get_dll_function_info(dll_path: str):
if is_native_dll(dll_path):
return get_native_function_info(dll_path)
else:
try:
loaded_file = load_dll(dll_path)
except Exception as e:
print(f"DLL情報の取得中にエラーが発生しました: {e}")
return []
return get_managed_function_info(loaded_file)
def load_managed_dll(dll_path: str) -> object:
"""マネージドDLLをロードする純粋関数"""
try:
return Assembly.LoadFrom(os.path.abspath(dll_path))
except Exception as e:
print(f"マネージド DLL のロードに失敗しました: {e}")
return None
def create_instance(assembly, class_name, *args):
"""
DLLから引数ありのコンストラクタを持つクラスのインスタンスを作成します。
Args:
dll_path (str): DLLファイルのパス。
class_name (str): インスタンス化するクラスの完全修飾名。
*args: コンストラクタに渡す引数。
Returns:
object: クラスのインスタンス。
"""
try:
type = assembly.GetType(class_name)
if type:
if args:
# 引数ありのコンストラクタを使用
constructor = type.GetConstructor([arg.__class__ for arg in args])
if constructor:
return constructor.Invoke(args)
else:
raise ValueError("指定された引数に一致するコンストラクタが見つかりません。")
else:
constructor = type.GetConstructor([]) # 空の引数リストで引数なしコンストラクタを取得
if constructor:
return constructor.Invoke([]) # 空の引数リストでコンストラクタを呼び出す
else:
raise ValueError("引数なしのコンストラクタが見つかりません。")
else:
raise ValueError(f"クラスが見つかりません: {class_name}")
except Exception as e:
raise RuntimeError(f"インスタンスの作成に失敗しました: {e}")
def get_managed_function_info(assembly: object) -> List[dict]:
"""
マネージドDLLからクラス単位の情報を取得する純粋関数です。
各クラスについて、インスタンス生成に使用するコンストラクタ情報(初期化引数名・型)と
各メソッド情報(メソッド名、引数名、引数型)をひとつのレコードにまとめて返します。
"""
function_infos = []
try:
for type in assembly.GetTypes():
if not type.IsPublic:
continue
# クラス情報のベースを作成
class_info = {
"class_name": type.FullName,
# コンストラクタ情報:ここでは、パラメータ数が最も多いコンストラクタを選択
"argument_names": [],
"argument_types": [],
# メソッド情報:各メソッドの情報をリストにまとめる
"methods": []
}
# コンストラクタ情報の取得(パブリックなコンストラクタを対象)
constructors = type.GetConstructors()
if constructors and len(constructors) > 0:
# 複数ある場合、パラメータ数が最も多いコンストラクタを選択(必要に応じて変更可)
chosen_ctor = max(constructors, key=lambda c: len(c.GetParameters()))
ctor_params = [(param.Name, param.ParameterType.Name) for param in chosen_ctor.GetParameters()]
class_info["init_argument_names"] = [p[0] for p in ctor_params]
class_info["init_argument_types"] = [p[1] for p in ctor_params]
# メソッド情報の取得(特殊メソッドは除外)
for method in type.GetMethods():
if method.IsSpecialName:
continue
params = [(param.Name, param.ParameterType.Name) for param in method.GetParameters()]
method_info = {
"method_name": method.Name,
"argument_names": [p[0] for p in params],
"argument_types": [p[1] for p in params]
}
class_info["methods"].append(method_info)
function_infos.append(class_info)
except Exception as e:
print(f"関数情報の取得中にエラーが発生しました: {e}")
return function_infos
def get_native_function_info(dll_path: object) -> list:
"""ネイティブDLLから関数情報を取得する純粋関数"""
function_infos = []
pe = pefile.PE(dll_path)
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if exp.name:
function_name = exp.name.decode('utf-8')
function_infos.append({
"class_name": None, # ネイティブDLLにはクラスがないため None を追加
"function_name": function_name,
"argument_names": [],
"argument_types": []
})
return function_infos
def convert_python_to_cs(value, cs_type: str):
"""
Python の値を C# 側で利用できる型に変換する関数。
例として、cs_type が "String[]" または "System.String[]" の場合は、
Python のリストを System.Array (System.String[]) に変換します。
"""
try:
if cs_type in ("int", "Int32"):
return int(value) if value is not None else 0
elif cs_type in ("double", "Double"):
return float(value) if value is not None else 0.0
elif cs_type in ("string", "String"):
return str(value) if value is not None else ""
elif cs_type in ("String[]", "System.String[]"):
# value が None なら空の文字列配列を作成
if value is None:
return System.Array.CreateInstance(System.String, 0)
# value がリストであれば、各要素を文字列に変換して配列化
if isinstance(value, list):
arr = System.Array.CreateInstance(System.String, len(value))
for i, v in enumerate(value):
arr[i] = str(v)
return arr
else:
# 単一値の場合も配列に変換
arr = System.Array.CreateInstance(System.String, 1)
arr[0] = str(value)
return arr
elif cs_type == "bool":
return bool(value)
elif cs_type == "Dictionary`2":
net_dict = NetDictionary[ String, Object]()
for key, item_value in value.items():
net_dict[key] = item_value # .NET Dictionary に値をセット
return net_dict
else:
# その他の型については、そのまま返すか、必要に応じて拡張する
return value
except Exception as e:
print(f"型変換エラー: {value} を {cs_type} に変換できません。エラー: {e}")
return value
def to_dotnet_dict(py_dict):
""" Python の辞書を .NET の Dictionary<String, Object> に変換 """
net_dict = NetDictionary[String, Object]()
for key, value in py_dict.items():
if isinstance(value, dict): # ネストされた dict は Dictionary<String, Object> に変換
net_dict[key] = to_dotnet_dict(value)
elif isinstance(value, list): # Python のリストは .NET の List<Object> に変換
net_list = List[Object]()
for item in value:
net_list.Add(item)
net_dict[key] = net_list
else:
net_dict[key] = value # そのまま格納(int, str, float, bool など)
return net_dict
# .NET の型を取得
NET_DICT_TYPE = clr.GetClrType(NetDictionary[String, Object])
NET_LIST_TYPE = clr.GetClrType(NetList[Object])
def convert_cs_to_python(obj):
""" .NET の Dictionary<String, Object> や List<Object> を Python の dict や list に変換 """
if obj == None:
return None
obj_type = clr.GetClrType(type(obj)) # .NET の型を取得
# obj の型をデバッグ用に出力
print(f"Result type: {obj_type.FullName}") # obj の型のフルネームを表示
if obj_type == NET_DICT_TYPE: # .NET Dictionary の場合
py_dict = {}
for key in obj.Keys:
py_dict[key] = convert_cs_to_python(obj[key]) # 再帰的に変換
return py_dict
elif obj_type == NET_LIST_TYPE: # .NET List の場合
return [convert_cs_to_python(item) for item in obj] # リスト内の要素を再帰的に変換
elif obj_type.FullName =="System.Boolean":
return obj
elif obj_type.FullName =="Python.Runtime.PyInt":
return obj
elif obj_type.FullName =="System.String":
return str(obj)
elif obj_type.FullName =="System.RuntimeType":
result = {
"Error": obj_type.FullName+"can not convert this object type",
"Result_type": obj_type.FullName # クラスの完全修飾名
}
return result # クラスの情報を返す
else:
result = {
"Error":obj_type.FullName+"can not convert this object type",
"Result_type": obj_type.FullName # クラスの完全修飾名
}
return result
テストプログラムを解析させると下記のような設定ファイルが出来上がります。
ClassとMethodの引数とその予測結果が記述されており、どんなテストを行うかが分かるはずです。
{
"test": [
{
"settings": {
"argument_names": [],
"argument_types": [],
"arguments": [],
"class_name": "ConfigSettingDiff.HelpEntry",
"methods": [
{
"argument_names": [
"obj"
],
"argument_types": [
"Object"
],
"arguments": [
{
"key": "value"
}
],
"check_result": {
"expected": false
},
"method_name": "Equals"
},
{
"argument_names": [],
"argument_types": [],
"arguments": [],
"check_result": {
"expected": 16495015
},
"method_name": "GetHashCode"
},
{
"argument_names": [],
"argument_types": [],
"arguments": [],
"check_result": {
"expected": {
"Error": "System.RuntimeTypecan not convert this object type",
"Result_type": "System.RuntimeType"
}
},
"method_name": "GetType"
},
{
"argument_names": [],
"argument_types": [],
"arguments": [],
"check_result": {
"expected": "ConfigSettingDiff.HelpEntry"
},
"method_name": "ToString"
}
],
"program_path": "../../../Tools/diff.dll"
},
"type": "ExecuteProgram "
},
・・・・以下略
テスト実行中の標準出力はこんな感じ。
ConfigSettingDiff.Program().GetHashCode()
Result type: Python.Runtime.PyInt
NG Result: 28880957, expected: 54025633
OverWrite the expected result setting with the actual results? :(W)
ConfigSettingDiff.Program().GetType()
Result type: System.RuntimeType
OK Result: {'Error': 'System.RuntimeTypecan not convert this object type', 'Result_type': 'System.RuntimeType'}
ConfigSettingDiff.Program().ToString()
Result type: System.String
OK Result: ConfigSettingDiff.Program
ConfigSettingDiff.Program().Main(String[] args=None)
Usage: ConfigDiff <json_setting> <help_folder> <config_folder1> <config_folder2> ...
NG Result: None, expected:
OverWrite the expected result setting with the actual results? :(W)
ConfigSettingDiff.Program().FolderSettingDiff(Dictionary`2 settings={},String helpFolder=None,String[] configFolders=['test'])
[DEBUG] Foldertest : Found 2 file(s) with filter '*.*'
Result type: ConfigSettingDiff.DiffResult
NG Result: {'Error': 'ConfigSettingDiff.DiffResultcan not convert this object type', 'Result_type': 'ConfigSettingDiff.DiffResult'}, expected:
OverWrite the expected result setting with the actual results? :(W)