0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

外部プログラムを実行するプログラム=>理想のプログラムテストにできるんじゃないか説 5

Last updated at Posted at 2025-05-17

プログラムとテストの歴史的な流れについて

ChatGPTに聞いてみた感じだとこんな感じのようです。

時代 プログラミングの主流 テストの特徴・課題 主なテスト思想や流行
1950–1960年代 アセンブリ言語/機械語 テスト=手動のデバッグ、ハードウェアとの密結合 プログラム完成後に手動でテスト(後付け)
1960–1970年代 高級言語(Fortran, COBOL, C)
サブルーチンの登場
再利用性が低く、関数単位テストが未確立 ホワイトボックス/ブラックボックステストの概念が登場
1980年代 構造化プログラミング(Pascal, Cなど) 関数単位のユニットテストが普及 モジュールテスト、統合テストなど体系化
1990年代 オブジェクト指向(C++, Java)
クラス・継承の導入
カプセル化で内部が見えにくくなり、ブラックボックス的に振る舞う GUIテストツール登場(WinRunner 等)、仕様ベースのテスト
2000年代 OOP + フレームワーク(.NET, Java EE) クラス間の依存が強くなりテスト困難 TDD(テスト駆動開発) が登場、xUnit 系ツールの普及
2010年代 関数型プログラミングの再評価(JavaScript, Scala) 副作用を避ける設計 → テストしやすい 関数型 + TDD/BDD、CI/CD による自動テストが一般化
2020年代〜現在 OOP×関数型ハイブリッド
AI/分散/非同期化の影響
テスト対象が複雑化(非同期、分散、MLなど) プロパティベーステスト、テストカバレッジ可視化、AIによる補助

なぜ2020年代に複雑化したのか?

原因一覧

  • 非同期・リアクティブ処理:async/await、Promiseなどで制御が複雑に
  • マイクロサービス/クラウド化:1台で完結しないシステム構成
  • 機械学習/AIの台頭:テストの正解が曖昧(期待結果が定義しづらい)
  • 継続的デリバリ:高速リリースに合わせてテストも即時性が求められる

複雑化への代表的な対策

1. プロパティベーステスト(Property-Based Testing)

  • 「入力値」ではなく「性質(property)」をテスト
  • ツールが自動で無数の入力を生成して性質を検証
  • 例:Hypothesis(Python)、fast-check(JS)

2. AIによるテスト補助・生成

  • LLM(GPTなど)によるテストケースの自動生成
  • UI操作の記録→再生
  • テストコードの自動補完・意図理解

3. 入力組み合わせの自動化

  • ペアワイズ法直交表で少数のケースから最大効果
  • ルールベースで候補を生成(例:入力名が「id」なら数字など)

4. テストの階層分割

内容 備考
ユニットテスト 関数・クラス単位、純粋なロジックの確認 TDDやプロパティベースが有効
統合テスト モジュール間のやり取りを検証 モックやスタブで依存を制御
E2Eテスト 実際のユーザー操作に近い流れで検証 Playwright/Cypress などを使用

5. ⛓️ テスト環境の自動再現(CI/CD + Docker)

  • 開発・テスト環境の統一(Dockerで固定化)
  • GitHub ActionsやGitLab CIなどで常に同じ状態で自動テスト

僕の目指すプログラムテスト

実はChat GPTに聞いて2020年以降の複雑化を見て驚いてしまってるのですが、
非同期とかAI、自動学習、複数PC、クラウドまでは想定せずに企画立ち上げてます。😅

その辺は僕の方法では難しいですので、今後の人類に頑張ってもらうとして。

ターゲットは2010年代〜の副作用を避けた関数型プログラミングです。
ChatGPT、Geminiなどの生成AIが生まれたので、新規からの作成が増えていると考えています。
その環境では基本的にテストがしやすい構造で作るはず。

それも、早い開発速度で。

そうすると、テスト環境は整備しとかないとまずいなという事で自作を考えたというわけです。
テストの思想はテスト駆動開発とプロパティベーステストの中間みたいなものです。

  • プロパティベーステストが自動でテストを生成するのに対して、人間がテストを手動設定する
    →Hypothesisなど自動テストでテストしないパターンを手動登録というのがいいのかもしれない。(Hypothesisはまだ触ってない。)
  • テスト駆動開発のように一個一個の関数に専用のテストパターンを設けるわけではない。共通のテストパターンを全ての関数に適用し、一律の動作精度を確保する考え。
  • 正常ケース、値異常ケース・・・のように種類別で引数のテストパターンを設定する
  • テスト生成時にはこの関数群(ファイル、フォルダ単位)に適切な種類のテストケース(正常、値異常・・・)ケースを適用するというように生成させる。(関数によっては全く見なくてもいい異常ケースというのもありえるから。)
    テストケースに名前が必要なのは最終的に人間が成否の判断をするので判断基準が必要な為。
test_pattern.json
{
    "description": "各テストケース(normal, value_errorなど)ごとに、関数の引数名や型に基づいてテスト値を生成するルールを定義します。テストケースは目的ごとに複数定義できます。",
    "normal": {
        "match_count": 2,
        "description": "function_match、argument_matchで一致した引数ruleでテストを行う。",
        "argument_rules": [
            {"type": "numeric", "argument_match": ["num", ".+count", ".+size", ".+length"],
             "value_list": [1, 2, 3, 4, 5]},
            {"type": "string", "argument_match": ["text"],
             "value_list": ["text1", "text2", "text3", "text4", "text5"]},

        ]
    },
    "value_error": {
        "match_count": 2,
        "argument_rules": [
            {"type": "Assembly", "argument_match": ["assembly"], "value_list": [null, 123, "string"]},
            {"type": "string", "argument_match": ["class_name"], "value_list": [null, 123, true]}
        ]
    }
}

このテストパターンをテストしたい関数に適用すると、下記のような実際のテスト設定が出来上がる。
test名にはテスト内容(正常、値異常・・)と何の関数かが書かれていれば、内容を察することができて、結果を判断する助けになるのではないかと。
※下記は1度テストを実行し、実行結果が予想結果(expected)に格納された状態。
※よく見たら同じ内容のテストが入ってるのはご愛敬😓
 重複削除も必要だけど、設定から重複してる可能性もあるなぁ・・・。設定が重複してるなら、重複分テスト増やすとかするべきだし、案外難しいかも。


{
    "test": [
        {
            "name": "test_normal_get_default_csc_path_./Sources/Common/DLLControl.py",
            "settings": {
                "argument_names": [],
                "argument_types": [],
                "arguments": [],
                "check_result": {
                    "expected": "C:\\Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe"
                },
                "class_name": "",
                "function_name": "get_default_csc_path",
                "program_path": "./Sources/Common/DLLControl.py",
                "result": "result_get_default_csc_path"
            },
            "type": "ExecuteProgram"
        },
        {
            "name": "test_value_error_get_default_csc_path_./Sources/Common/DLLControl.py",
            "settings": {
                "argument_names": [],
                "argument_types": [],
                "arguments": [],
                "check_result": {
                    "expected": "C:\\Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe"
                },
                "class_name": "",
                "function_name": "get_default_csc_path",
                "program_path": "./Sources/Common/DLLControl.py",
                "result": "result_get_default_csc_path"
            },
            "type": "ExecuteProgram"
        },
        {
            "name": "test_value_error_get_default_csc_path_./Sources/Common/DLLControl.py",
            "settings": {
                "argument_names": [],
                "argument_types": [],
                "arguments": [],
                "check_result": {
                    "expected": "C:\\Windows/Microsoft.NET/Framework/v4.0.30319/csc.exe"
                },
                "class_name": "",
                "function_name": "get_default_csc_path",
                "program_path": "./Sources/Common/DLLControl.py",
                "result": "result_get_default_csc_path"
            },
            "type": "ExecuteProgram"
        },
        {
            "name": "test_normal_compile_source_to_dll_./Sources/Common/DLLControl.py",
            "settings": {
                "argument_names": [
                    "cs_file_path",
                    "output_dll_path",
                    "build_method",
                    "cs_compiler_path",
                    "references"
                ],
                "argument_types": [
                    "string",
                    "string",
                    "string",
                    "string",
                    "string[]"
                ],
                "arguments": [
                    "./Sources/Test/valid_code.cs",
                    "./mydll.dll",
                    "csc",
                    "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe",
                    [
                        "System.dll"
                    ]
                ],
                "check_result": {
                    "expected": false
                },
                "class_name": "",
                "function_name": "compile_source_to_dll",
                "program_path": "./Sources/Common/DLLControl.py",
                "result": "result_compile_source_to_dll"
            },
            "type": "ExecuteProgram"
        },
        {
            "name": "test_normal_compile_source_to_dll_./Sources/Common/DLLControl.py",
            "settings": {
                "argument_names": [
                    "cs_file_path",
                    "output_dll_path",
                    "build_method",
                    "cs_compiler_path",
                    "references"
                ],
                "argument_types": [
                    "string",
                    "string",
                    "string",
                    "string",
                    "string[]"
                ],
                "arguments": [
                    "./dummy.dll",
                    "./mydll.dll",
                    "csc",
                    "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe",
                    [
                        "System.dll"
                    ]
                ],
                "check_result": {
                    "expected": false
                },
                "class_name": "",
                "function_name": "compile_source_to_dll",
                "program_path": "./Sources/Common/DLLControl.py",
                "result": "result_compile_source_to_dll"
            },
            "type": "ExecuteProgram"
        },
                    <以下略>

実際に使っている感じだと、残念ながら、結果が正しいかは人間が目で見て確認する必要があり、あまり多くのテストを実施するのには向いていないようだ。

実施するテストの種類や、テストの組み合わせを制御する設定でテストの数を制御しないと、せっかくテストしても現実的にはテスト結果を確認できないようになると思えました。
下記のようなテストの制御設定も必要と考えてます。

{
    "scenarios": ["normal", "value_error"],
    "combination_mode": "cartesian"
}
※他、テストパターンのうち何個までテストするみたいな設定が必要な感あり。
test_patternの"match_count": 2はそのための記述。
ただ、本当はテストの制御設定側で設定したいので、そのうち変更するかも

<案1>冗長だけど分かりやすい?
"scenarios": [{"name":"normal","match_count":2}, {"name":"value_error","match_count":3}],

<案2>簡単な記述。
"scenarios": ["normal", "value_error"],
"scenarios_execute_number": [3,0],

次に、テストパターンを作るための補助機能も必要。
自動で設定にマッチングしたテストパターンを作るので、マッチングしなかったパターンも明示する必要がある。
下記のようなミスマッチングの関数、引数をテストパターンの形式でファイル保存する事で簡単にテストパターンを補充できるはず。

{
  "test": [
    {
      "function_match": "compile_source_to_dll",
      "argument_match": "cs_file_path",
      "value_list": [
        "please set value"
      ]
    },
    {
      "function_match": "compile_source_to_dll",
      "argument_match": "cs_file_path",
      "value_list": [
        "please set value"
      ]
    },・・・
  ],
  "test_dll": [
    {
      "function_match": "Equals",
      "argument_match": "obj",
      "value_list": [
        "please set value"
      ],
      "type": "Object"
    },
    {
      "function_match": "Equals",
      "argument_match": "obj",
      "value_list": [
        "please set value"
      ],
      "type": "Object"
    },・・・・・
    
  ]
}

そして、いつもながら動作確認中途半端+簡単な独自参照ライブラリ省略のソースを挙げます。
あと、だいぶ冗長なので短くできないもんか・・・?

FunctionControl.py
import ast
import inspect
import random
import json
import importlib.util
import os
import sys
import JSON_Control
from typing import List, Any
import ListControl
import DLLControl
import clr
import re
import itertools
import copy
import SharedMemory
import SettingUtility
import ClassControl
import DictionaryControl
import traceback

class ExternalFunctionError(Exception):
    """外部関数呼び出しに失敗したことを示す例外"""
    pass

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ファイルをロードする純粋関数"""
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            return ast.parse(file.read())
    except FileNotFoundError:
        raise FileNotFoundError(f"ファイルが見つかりません: {file_path}")
    except SyntaxError as e:
        raise SyntaxError(f"ファイルの構文エラー: {file_path}, エラー: {e}")
    
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

# ====================================================
# 1. 型推論&テスト値生成(設定ベース・マッチ数対応)
# ====================================================
import re

def infer_possible_types_and_values(func_name, arg_name, arg_index, scenario, settings, arg_type=None):
    """
    指定シナリオ("normal" や "value_error")の設定から、
    引数名と引数位置を考慮して、マッチするルール候補を全件取得する。

    "function_match" があれば、関数名にマッチしたときだけルールを適用する。
    "function_match" がなければ、全関数に対して適用される。

    ルール内で "arg_index" が設定されている場合は、該当位置にのみ適用される。
    """
    scenario_conf = settings.get(scenario, {})
    max_matches = scenario_conf.get("match_count", 1)
    rules = scenario_conf.get("argument_rules", [])  # or "rules" depending on naming convention

    lower_name = arg_name.lower()
    candidates = []

    for rule in rules:
        # 型が指定されていれば一致するもののみ対象
        if arg_type and arg_type != rule.get("type"):
            continue

        # arg_index が指定されている場合、そのインデックスと一致する必要がある
        if "arg_index" in rule and rule["arg_index"] != arg_index:
            continue

        # function_match が存在する場合は関数名がマッチする必要がある
        function_patterns = rule.get("function_match")
        if function_patterns:
            if not isinstance(function_patterns,list):
                function_patterns = [function_patterns]
            if not any(re.search(pat, func_name) for pat in function_patterns):
                continue  # マッチしなければスキップ

        # 引数名がマッチするか確認
        for pattern in rule.get("argument_match", []):
            if re.search(pattern, lower_name):
                candidates.append((rule["type"], rule["value_list"]))
                break  # 1ルール内でマッチしたら次のルールへ

        if len(candidates) >= max_matches:
            break
    
    if not candidates:
        missing = {
            "function_match": func_name,
            "argument_match": arg_name,
            "value_list": ["please set value"]
        }
        if arg_type:
            missing["type"] = arg_type
        return [("unknown", [None])], missing

    return candidates, []

def generate_argument_candidate_list(func_name, arg_name, arg_index, scenario, settings, argument_types=None):
    """
    引数名・位置・シナリオおよび設定に基づいて候補リストを作成する。
    argument_types が与えられている場合はそれを優先して値リスト(デフォルト値)を返す。

    結果は候補リスト [(型, 値), …] となり、各ルールの value_list から展開します。
    """
    candidates, missing = infer_possible_types_and_values(func_name, arg_name, arg_index, scenario, settings, argument_types)
    candidate_list = []

    for _type, values in candidates:
        if not values:
            candidate_value = None
        else:
            candidate_value = values[arg_index % len(values)]
        candidate_list.append((_type, candidate_value))

    return candidate_list, missing


# ====================================================
# 2. 組み合わせ生成方法
# ====================================================
def generate_combinations(lists, mode="cartesian", random_sample_size=None):
    """
    組み合わせ生成。mode による対応は以下の通り:
      - "cartesian":各引数のすべての値の直積をそのまま生成
      - "pairwise":簡易ペアワイズ。最初の2引数間+残りの直積
      - "random": 全組み合わせから指定数だけランダムサンプル抽出
      - "orthogonal": 直交配列表(簡易実装例:各リストから等間隔に選択)
    """
    if mode == "cartesian":
        return list(itertools.product(*lists))
    elif mode == "pairwise":
        if not lists:
            return [[]]
        elif len(lists) == 1:
            return [[item] for item in lists[0]]
        else:
            first_pair = list(itertools.product(lists[0], lists[1]))
            remaining = lists[2:]
            if remaining:
                rest_combs = list(itertools.product(*remaining))
                return [fp + rest for fp in first_pair for rest in rest_combs]
            else:
                return first_pair
    elif mode == "random":
        all_comb = list(itertools.product(*lists))
        if random_sample_size and random_sample_size < len(all_comb):
            return random.sample(all_comb, random_sample_size)
        else:
            return all_comb
    elif mode == "orthogonal":
        result = []
        if not lists:
            return [[]]
        min_len = min(len(lst) for lst in lists if lst)
        for i in range(min_len):
            result.append(tuple(lst[i] for lst in lists))
        return result
    else:
        return []

# ====================================================
# 3. テストケース作成
# ====================================================

def generate_test_cases(function_informations, settings, 
                        combination_mode=None, random_sample_size=None):
    """
    ・function_informations に "methods" が存在すればクラスとして扱い、  
      コンストラクタ引数と各メソッド引数の候補を生成してテストケースを作成します。  
    ・各引数は、引数名、位置、型情報(あれば)と設定から候補リストを生成し、  
      その組み合わせによりテストケース(辞書)を作成します。
    
    settings からは、テストシナリオ("scenarios")や  
    組み合わせモード("combination_mode")が利用されます。
    """
    scenarios = [key for key, value in settings.items() if isinstance(value, dict)]
    if not combination_mode:
        combination_mode = settings.get("combination_mode", "cartesian")
        
    test_cases = []
    missing_patterns = []
    for info in function_informations:
        program_path = info.get("program_path", "")
        if "methods" in info:  # クラスの場合
            class_name = info.get("class_name", "")
            init_arg_names = info.get("init_argument_names", [])
            # 各シナリオごとのコンストラクタ引数候補
            init_candidates_by_scenario = {scenario: [] for scenario in scenarios}
            init_types = []  # 型情報(argument_types 指定または最初の候補)
            for index, arg in enumerate(init_arg_names):
                for scenario in scenarios:
                    argument_types = None
                    if info.get("init_argument_types", [None]*len(init_arg_names))[index]:
                        argument_types = info["init_argument_types"][index]
                    cand ,missing= generate_argument_candidate_list(class_name,arg, index, scenario, settings, argument_types=argument_types)
                    if missing:
                        missing_patterns.append(missing)
                    init_candidates_by_scenario[scenario].append(cand)
                # 1シナリオ目の候補から採用
                if info.get("init_argument_types", [None]*len(init_arg_names))[index]:
                    init_types.append(info["init_argument_types"][index])
                else:
                    init_types.append(init_candidates_by_scenario[scenarios[0]][index][0][0])
            
            methods_info = info.get("methods", [])
            method_tests_lists_by_scenario = {scenario: [] for scenario in scenarios}
            
            for method in methods_info:
                method_name = method.get("method_name", "")
                m_arg_names = method.get("argument_names", [])
                m_candidates_by_scenario = {scenario: [] for scenario in scenarios}
                m_types = []
                for index, arg in enumerate(m_arg_names):
                    for scenario in scenarios:
                        argument_types = None
                        if method.get("argument_types", [None]*len(m_arg_names))[index]:
                            argument_types = method["argument_types"][index]
                        cand , missing = generate_argument_candidate_list(method_name,arg, index, scenario, settings, argument_types=argument_types)
                        if missing:
                            missing_patterns.append(missing)
                        m_candidates_by_scenario[scenario].append(cand)
                    if method.get("argument_types", [None]*len(m_arg_names))[index]:
                        m_types.append(method["argument_types"][index])
                    else:
                        m_types.append(m_candidates_by_scenario[scenarios[0]][index][0][0])
                # 各シナリオごとに組み合わせ生成
                for scenario in scenarios:
                    m_comb = generate_combinations(m_candidates_by_scenario[scenario], mode=combination_mode, random_sample_size=random_sample_size)
                    method_tests = []
                    for comb in m_comb:
                        test_setting = {
                            "name":f"test_{scenario}_{method_name}",
                            "method_name": method_name,
                            "argument_names": m_arg_names,
                            "argument_types": m_types,
                            "arguments": [x[1] for x in comb],
                            "check_result": {},
                            "result": "result_" + method_name
                        }
                        method_tests.append(test_setting)
                    method_tests_lists_by_scenario[scenario].append(method_tests)
            
            # クラス全体のテストケース生成(コンストラクタとメソッドの直積)
            for scenario in scenarios:
                init_comb = generate_combinations(init_candidates_by_scenario[scenario], mode=combination_mode, random_sample_size=random_sample_size)
                if method_tests_lists_by_scenario[scenario]:
                    methods_all = list(itertools.product(*method_tests_lists_by_scenario[scenario]))
                else:
                    methods_all = [[]]
                for i_comb in init_comb:
                    for m_comb in methods_all:
                        test_case = {
                            "type": "ExecuteProgram",
                            "name":f"test_{scenario}_{class_name}_{program_path}",
                            "settings": {
                                "program_path": program_path,
                                "class_name": class_name,
                                "argument_names": init_arg_names,
                                "argument_types": init_types,
                                "arguments": [x[1] for x in i_comb],
                                "methods": list(m_comb),
                                "check_result": {},
                                "result": "result_" + class_name
                            }
                        }
                        test_cases.append(test_case)
        else:
            # グローバル関数の場合
            function_name = info.get("function_name", "")
            argument_names = info.get("argument_names", [])
            arg_candidates_by_scenario = {scenario: [] for scenario in scenarios}
            arg_types = []
            for index, arg in enumerate(argument_names):
                arg_type=""
                for scenario in scenarios:
                    argument_types = None
                    if info.get("argument_types", [None]*len(argument_names))[index]:
                        argument_types = info["argument_types"][index]
                for scenario in scenarios:
                    cand , missing = generate_argument_candidate_list(function_name,arg, index, scenario , settings, argument_types=argument_types)
                    if missing:
                        missing_patterns.append(missing)
                    arg_candidates_by_scenario[scenario].append(cand)
                    if cand[0][0] != "unknown":
                        arg_type=cand[0][0]
                if info.get("argument_types", [None]*len(argument_names))[index]:
                    arg_types.append(info["argument_types"][index])
                else:
                    arg_types.append(arg_type)
            for scenario in scenarios:
                combinations = generate_combinations(arg_candidates_by_scenario[scenario], mode=combination_mode, random_sample_size=random_sample_size)
                for combination in combinations:
                    test_case = {
                        "type": "ExecuteProgram",
                        "name":f"test_{scenario}_{function_name}_{program_path}",
                        "settings": {
                            "program_path": program_path,
                            "function_name": function_name,
                            "class_name": info.get("class_name", ""),
                            "argument_names": argument_names,
                            "argument_types": arg_types,
                            "arguments": [x[1] for x in combination],
                            "check_result": {},
                            "result": "result_" + function_name
                        }
                    }
                    test_cases.append(test_case)
    return test_cases,missing_patterns
# ====================================================
# 4. テストケース出力
# ====================================================

def save_missing_argument_patterns(name,missing_patterns, filepath="missing_argument_patterns.json"):
    """
    既存のファイルがある場合は読み出して、{name: missing_patterns} 形式で結合して保存。
    """
    if not missing_patterns:
        print("未マッチ引数パターンはありませんでした。")
        return

    # 既存ファイルの読み込み
    if os.path.exists(filepath):
        with open(filepath, "r", encoding="utf-8") as f:
            try:
                read_data = json.load(f)
                if not isinstance(read_data,dict):
                    existing_data = {"old_data": read_data}
                else:
                    existing_data = read_data

            except json.JSONDecodeError:
                existing_data = {}
            
    else:
        existing_data = {}
    # name(テストケース名)で上書き・追加
    existing_data[name] = missing_patterns

    # 保存
    with open(filepath, "w", encoding="utf-8") as f:
        json.dump(existing_data, f, indent=2, ensure_ascii=False)

    print(f"{name} の未マッチ引数パターンを {filepath} に保存しました。")

def write_test_cases(program_path, output_file_path, test_data_name, test_pattern, test_pattern_path = "" , missing_pattern_path = "", options={}):
    # 設定ファイルが指定されている場合は読み込む
    if test_pattern_path and os.path.exists(test_pattern_path):
        with open(test_pattern_path, "r", encoding="utf-8") as f:
            base_test_pattern = json.load(f)
    else:
        base_test_pattern = {}

    # test_pattern で base_test_pattern を上書き(base_test_pattern をベースにする)
    merged_test_pattern = base_test_pattern.copy()
    merged_test_pattern.update(test_pattern)

    # 関数情報取得
    function_informations = get_function_info(program_path, options)
    if not function_informations:
        return

    # テストケース生成
    test_cases , missing_patterns = generate_test_cases(function_informations, merged_test_pattern)

    # テスト計画としてまとめる
    test_plan_list = {test_data_name: test_cases}

    # JSONファイルに出力
    JSON_Control.WriteDictionary(output_file_path, test_plan_list)
    # missing_patterns を保存
    save_missing_argument_patterns(test_data_name,missing_patterns, missing_pattern_path)

    return test_plan_list


# --- グローバル関数用テスト実行 ---
def execute_function(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:
                    argument_values = ListControl.replace_list_values(argument_values,settings) 
                    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:
                        try:
                            result_value = method.Invoke(None, converted_args)
                        except Exception as e:
                            raise ExternalFunctionError(f"execute_functionの呼出先{class_name}().{function_name}でエラーが発生しました: {e}") from e  
                    else:
                        # インスタンス生成(デフォルトコンストラクタ)してメソッド実行
                        instance = DLLControl.create_instance(assembly, class_name, *[])
                        result_value = instance.GetType().GetMethod(function_name).Invoke(instance, converted_args)
                    return result_value
                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)
            argument_values = ListControl.replace_list_values(argument_values,settings) 
            format_str = ",".join(ListControl.format_merge_multiple_list(
                "{list1} = {list2}", "",
                list1=argument_names, list2=argument_values))
            print(f"Function: {function_name}({format_str})")
            try:
                result_value = function(*argument_values)
            except Exception as e:
                raise ExternalFunctionError(f"execute_functionの呼出先{function_name}でエラーが発生しました: {e}") from e     
        return result_value
    except FileNotFoundError:
        raise Exception(f"ファイルが見つかりません: {os.path.abspath(program_path)}")
    except AttributeError as e:
        raise Exception(f"クラスのインスタンス化またはメソッド呼び出しに失敗しました: {e}")


# --- クラステスト用:インスタンス生成+各メソッド実行 ---
def execute_class(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", "")
                argument_names = method_setting.get("argument_names", [])
                argument_types = method_setting.get("argument_types", [])
                argument_values = method_setting.get("arguments", [])
                argument_values = ListControl.replace_list_values(argument_values,method_setting)
                fmt = ",".join(ListControl.format_merge_multiple_list("{list3} {list1}={list2}", "", list1=argument_names, list2=argument_values,list3 = argument_types))
                converted_m_args = [DLLControl.convert_python_to_cs(val, typ) for val, typ in zip(argument_values, 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}")
                try:
                    cs_result = method.Invoke(instance, converted_m_args)
                except Exception as e:
                    raise ExternalFunctionError(f"execute_classの呼出先{class_name}().{method_name}でエラーが発生しました: {e}") from e      
                result = DLLControl.convert_cs_to_python(cs_result)
                method_setting = CheckResult(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", "")
                argument_names = method_setting.get("argument_names", [])
                argument_types = method_setting.get("argument_types", [])
                argument_values = method_setting.get("arguments", [])
                argument_values = ListControl.replace_list_values(argument_values,method_setting)
                fmt = ",".join(ListControl.format_merge_multiple_list("{list1}={list2}", "", list1=argument_names, list2=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)
                try:
                    result = method(*argument_values)
                except Exception as e:
                    raise ExternalFunctionError(f"execute_classの呼出先{class_name}().{method_name}でエラーが発生しました: {e}") from e    
                method_setting = CheckResult(method_setting,result)
        settings["methods"] = methods 
        return  settings         
    except FileNotFoundError:
        print(f"ファイルが見つかりません: {program_path}")
    except AttributeError as e:
        print(f"クラスのインスタンス生成またはメソッド呼び出しに失敗しました: {e}")


def create_instance(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", [])
    
    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}")
        init_argument_values = ListControl.replace_list_values(init_argument_values,settings)
        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)
    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")
        init_argument_values = ListControl.replace_list_values(init_argument_values,settings)
        instance = target(*init_argument_values)
    
    return instance

def execute_methods_cs(instance,  method_setting):
    method_name = method_setting.get("method_name", "")
    argument_types = method_setting.get("argument_types", [])
    argument_values = method_setting.get("arguments", [])
    
    converted_m_args = [DLLControl.convert_python_to_cs(val, typ) for val, typ in zip(argument_values, argument_types)]
    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)
    return result

def execute_methods_python(instance,  method_setting):
    method_name = method_setting.get("method_name", "")
    argument_values = method_setting.get("arguments", [])
    if not hasattr(instance, method_name):
        raise AttributeError(f"メソッドが見つかりません: {method_name}")
    method = getattr(instance, method_name)
    result = method(*argument_values)
    method_setting = CheckResult(method_setting,result)
    return result




def method_format(method_setting):
    method_name = method_setting.get("method_name", "")
    argument_names = method_setting.get("argument_names", [])
    argument_types = method_setting.get("argument_types", [])
    argument_values = method_setting.get("arguments", [])

    fmt = ",".join(ListControl.format_merge_multiple_list("{list3} {list1}={list2}", "", list1=argument_names, list2=argument_values, list3=argument_types))
    return f"{method_name}({fmt})"

# --- テストケース全体の実行 ---
def ExecuteProgram(settings):
    """テストケースに基づいてプログラムを実行する"""
    class_name = settings.get("class_name", "")
    methods = settings.get("methods", [])
    program_path = settings.get("program_path", "")
    result_dictionary={}
    if methods:
        try:
            if program_path.lower().endswith(".dll"):
                instance = create_instance(settings)
                for i, method_setting in enumerate(methods):
                    fmt = method_format(method_setting)
                    print(f"{program_path}-{i} {class_name}.{fmt}")
                    try:
                        result_value = execute_methods_cs(instance, method_setting)
                    except Exception as e:
                        trace = traceback.format_exc()
                        result_value = {"success": False, "error": str(e),"trace":trace}
                    methods[i] = CheckResult(method_setting, result_value)
                    result_settings = method_setting.get("result")
                    if result_settings:
                        WriteDatas(result_settings,result_dictionary,result_value)
            else:
                instance = create_instance(settings)
                for method_setting in methods:
                    fmt = method_format(method_setting)
                    print(f"{program_path}-{class_name}.{fmt}")
                    try:
                        result_value = execute_methods_python(instance, method_setting)
                    except Exception as e:
                        trace = traceback.format_exc()
                        result_value = {"success": False, "error": str(e),"trace":trace}

                    method_setting = CheckResult(method_setting,result_value)
                    result_settings = method_setting.get("result")
                    if result_settings:
                        WriteDatas(result_settings,result_dictionary,result_value)
            return result_dictionary
        except FileNotFoundError:
            raise Exception(f"ファイルが見つかりません: {settings.get('program_path', '')}")
        except AttributeError as e:
            raise Exception(f"クラスのインスタンス生成またはメソッド呼び出しに失敗しました: {e}")
    else:
        return execute_function(settings)
        
        

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 RunPlanLists(settings,data_store={},plan_lists={}):
    data_store = settings.get("data_store",data_store)
    plan_lists = settings.get("plan_lists",plan_lists)
    plan_lists_file_path = settings.get("plan_lists_file_path","")
    if plan_lists_file_path:
        details = {}
        read_plan_lists = JSON_Control.ReadDictionary(plan_lists_file_path,{},details=details)
        if details.get("success",True) == False:
            print(str(details))
            input("file error.please push and skip."+plan_lists_file_path)
        if read_plan_lists:
            plan_lists = read_plan_lists
    run_plan_name_list = settings.get("run_plan_name_list")
    if run_plan_name_list == "":
        details = {"success":False,"error":"run_plan_name_list is nothing."}
        return details
    if not isinstance(run_plan_name_list,list):
        run_plan_name_list = [run_plan_name_list]
    for run_plan_name in run_plan_name_list:        
        plan_list = plan_lists.get(run_plan_name,"")
        if plan_list:
            if not isinstance(plan_list,list):
                plan_list=[plan_list]
            for num,plan in enumerate(plan_list):
                plan_settings = {
                    "run_plan_name":run_plan_name,
                    "run_plan_number": num
                }
                if plan_lists_file_path:
                    plan_settings["plan_lists_file_path"] = plan_lists_file_path
                result = ExecutePlan(plan_settings,data_store,plan_lists)

        
def ExecutePlan(settings,data_store={},plan_lists={}):
    data_store = settings.get("data_store",data_store)
    plan_lists = settings.get("plan_lists",plan_lists)
    # 実行する設定の読込
    run_plan_name = settings.get("run_plan_name","")
    run_plan_number = settings.get("run_plan_number","")

    plan_list = plan_lists.get(run_plan_name)
    if not plan_list:
        return {"success":False,"error":"plan_list is nothing"}
    if not isinstance(plan_list,list):
        plan_list=[plan_list]
    try:
        plan = plan_list[run_plan_number]
    except Exception as e:
        return {"success":False,"error":"plan is nothing."+ str(e)}
    plan_copy = copy.deepcopy(plan)
    
    plan_type= plan_copy.get("type","Action")
    plan_settings = plan_copy.get("settings",{})        
    check_result = plan_settings.get("check_result",None)
    # 実行するPlan内容の表示
    if check_result is not None:
        plan_name =plan_copy.get("name","")
        message= "Plan:"+plan_name+"("+str(settings)+")" 
        print(message)
    # referenceの内容をdata_store,shared_memoryから参照して反映する。      
    apply_reference_values(plan_settings,data_store,shared_memory_file_path="")
    #関数を実行する
    try:
        current_module = inspect.getmodule(inspect.currentframe())
        loaded_function = ClassControl.load_function_from_module(current_module,plan_type)
        plan_settings_backup = copy.deepcopy(plan_settings)
        # シグネチャを解析して引数の個数をチェック
        sig = inspect.signature(loaded_function)
        parameter_names = list(sig.parameters.keys())
        parameter_number = len(parameter_names)
        # 引数の数に応じて関数を呼び出す
        if parameter_number == 3:
            result_value = loaded_function(plan_settings, data_store,plan_lists)
        elif parameter_number == 2:
            result_value = loaded_function(plan_settings, data_store)
        else:
            result_value = loaded_function(plan_settings)
    except Exception as e:
        message = str(e)
        trace = traceback.format_exc()
        result_value = {"success": False, "error": message,"trace":trace}
        print(message)
        print(trace)
    check_result = plan_settings.get("check_result",None)
    if check_result is not None:
        check_result_settings ={
            "result_value":result_value,
            "check_result":check_result                    
        }
        CheckResult(check_result_settings)
    # plan実行後のsettingsのFeed Back内容を反映する。
    result_diff = DictionaryControl.DiffDictionaries(plan_settings_backup,plan_settings)
    if result_diff:
        plan["settings"] = DictionaryControl.ChangeDictionary(result_diff,plan.get("settings",{}))
        plan_list_file_path = settings.get("plan_lists_file_path","")
        if plan_list_file_path:
            read_plan_lists = JSON_Control.ReadDictionary(plan_list_file_path,{})
            if read_plan_lists:
                read_plan_list = read_plan_lists.get(run_plan_name,[])
                if read_plan_list:
                    if isinstance(plan_list,list):
                        read_plan_list[run_plan_number] = plan
                    else:
                        read_plan_list = plan                        
                    JSON_Control.WriteDictionary(plan_list_file_path, read_plan_lists)

    result_settings = plan_settings.get("result")
    if result_settings:
        WriteDatas(result_settings,data_store,result_value)

def CheckResult(settings,result_value={}):
    check_result = settings.get("check_result", None)
    if check_result == None:
        return settings 
    if not result_value:
        result_value = settings.get("result_value")
    if check_result=={}:
        if isinstance(result_value, (str, list, dict, int, float, bool, type(None))): 
            check_result["expected"] = result_value
        else :
            check_result["expected"] = {"error":"The result cannot write to JSON.","result_type":type(result_value).__name__}            
        settings["check_result"] = check_result
        print(f"Write expected: {result_value}")

    elif check_result:
        expected = check_result.get("expected")
        if result_value == expected:
            print(f"OK Result: {result_value}")
        else:
            print(f"NG Result: {result_value}, expected: {expected}")
            user_input = input("OverWrite the expected result setting with the actual results? :(W) ")
            if user_input.upper() == "W":
                if isinstance(result_value, (str, list, dict, int, float, bool, type(None))): 
                    check_result["expected"] = result_value
                else :
                    check_result["expected"] = {"error":"The result cannot write to JSON.","result_type":type(result_value).__name__}
                settings["check_result"] = check_result
    return settings 
def WriteDatas(settings,data_store,datas):
    if isinstance(settings,str):
        key = settings
        data_store[key] = datas
        print("WriteDatas("+key+":"+str(datas)+")")
    if isinstance(settings,dict):
        # 設定に基づいてデータを抽出して保存
        for key,data_key in settings.items():
            if data_key in datas:
                value =datas[data_key]
                data_store[key] = value
                print("WriteDatas("+key+":"+str(value)+")")

def apply_reference_values(settings,data_store,shared_memory_file_path=""):
    reference_list = settings.get("reference", [])
    if not reference_list:
        return settings
    if not isinstance(reference_list,list):
        reference_list =[reference_list]
    for reference in reference_list:
        if isinstance(reference,dict):
            source_type = reference.get("source_type","data_store")
            if source_type =="share_memory":
                shared_memory = SharedMemory.read_from_shared_memory(shared_memory_file_path,{})
                shared_memory_name = settings.get("shared_memory_name","shared_memory_name")
                shared_data=shared_memory.get(shared_memory_name,{})
                for key, reference_key in reference.items():
                    value = shared_data.get(reference_key, "")
                    if value:
                        settings[key] = value
                    else:
                        value = data_store.get(reference_key,"")
                        if value:
                            settings[key] = value
            elif source_type =="data_store":
                for key, reference_key in reference.items():
                    value = data_store.get(reference_key,"")
                    if value:
                        settings[key] = value

def main():
    plan_lists = {
        "test_plan":[
            {
                "type":"ExecuteProgram",
                "settings":{
                    "arguments":["test_program_path", "output_filename", "data_name", "test_pattern","test_pattern_path","missing_pattern_path", "options"],
                    "program_path" : "./Sources/Common/FunctionControl.py",
                    "test_program_path" : "./Sources/Common/DLLControl.py",
                    "function_name" : "write_test_cases",
                    "output_filename" : "Test_DLLControl.json",
                    "data_name" : "test",
                    "test_pattern_path":"test_pattern.json",
                    "missing_pattern_path":"missing_pattern.json",
                    "test_pattern": {
                    },
                    "scenarios": ["normal", "value_error"],
                    "combination_mode": "cartesian",
                    "options": {"references": [".\\Reference\\Newtonsoft.Json.13.0.3\\lib\\net35\\Newtonsoft.Json.dll"]}
                }
            },
            {
                "type":"RunPlanLists",
                "settings" :{
                    "plan_lists_file_path":"Test_DLLControl.json",
                    "run_plan_name_list":"test",
                    "data_store" : {"key1":"value1","key2":"value2"}
                }
            }
        ]
    }

    
    # Pythonのテスト
    settings = {
        "plan_lists": plan_lists,
        "run_plan_name_list": "test_plan",
    }
        
    RunPlanLists(settings)

    # DLL のテスト
    plan_lists["test_plan"][0]["settings"]["test_program_path"]= "../../../Tools/diff.dll"
    plan_lists["test_plan"][0]["settings"]["output_filename"]= "Test_dll.json"
    plan_lists["test_plan"][0]["settings"]["data_name"]= "test_dll"
    plan_lists["test_plan"][1]["settings"]["plan_lists_file_path"]= "Test_dll.json"
    plan_lists["test_plan"][1]["settings"]["run_plan_name_list"]= "test_dll"

    RunPlanLists(settings)

      
if __name__ == "__main__":
    main()

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?