import re import ast from typing import Dict, Any, List, Optional, Set, Tuple from validators.base import BaseValidator, Validator class TestGenerator(BaseValidator, Validator): """テストケース生成を行うクラス""" def __init__(self, client): """テスト生成クライアントを初期化""" super().__init__(client) # テスト生成用のモデル self.test_model = "qwen-2.5-coder-32b" # テスト生成に特化したモデル self.generated_tests = None async def validate(self, code, context=None): """テスト生成を実行する(インターフェース実装)""" self.generated_tests = await self.generate_comprehensive_tests(code, context) return self.generated_tests def get_result_summary(self): """テスト生成結果の要約を返す(インターフェース実装)""" if self.generated_tests is None: return "テストはまだ生成されていません。" # テストの基本情報を抽出して要約を生成 lines = self.generated_tests.split("\n") test_count = sum(1 for line in lines if line.strip().startswith("def test_")) return f"{test_count}個のテストケースが生成されました。" async def generate_comprehensive_tests(self, code: str, requirements: str = None, language: str = "python") -> str: """要件とコードから包括的なテストを生成する""" # コードの解析 test_analysis = self._analyze_code_for_testing(code, language) # テスト生成プロンプトの構築 prompt = self._build_test_generation_prompt(code, test_analysis, requirements, language) # APIコールの実行 messages = [ { "role": "system", "content": prompt } ] response = await self._make_async_api_call( self.test_model, messages, temperature=0.3, max_tokens=2500 ) # 生成されたテストコード test_code = response.choices[0].message.content return test_code def _analyze_code_for_testing(self, code: str, language: str) -> Dict[str, Any]: """テスト生成のためのコード解析を行う""" result = { "functions": [], "classes": [], "edge_cases": [], "identified_patterns": [], "complexity_analysis": {} } if language == "python": self._analyze_python_code(code, result) elif language == "javascript": self._analyze_javascript_code(code, result) elif language == "java": self._analyze_java_code(code, result) elif language == "cpp" or language == "c++": self._analyze_cpp_code(code, result) # エッジケースとパターンの特定 self._identify_edge_cases_and_patterns(code, result, language) return result def _analyze_python_code(self, code: str, result: Dict[str, Any]) -> None: """Pythonコードの解析を行う""" try: tree = ast.parse(code) # 関数の抽出 for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): function_info = { "name": node.name, "args": [], "returns": None, "docstring": ast.get_docstring(node), "is_method": False, "decorators": [d.id if isinstance(d, ast.Name) else "" for d in node.decorator_list] } # 引数の抽出 for arg in node.args.args: arg_info = {"name": arg.arg, "type": None} if arg.annotation: if isinstance(arg.annotation, ast.Name): arg_info["type"] = arg.annotation.id elif isinstance(arg.annotation, ast.Subscript): if isinstance(arg.annotation.value, ast.Name): arg_info["type"] = f"{arg.annotation.value.id}[...]" function_info["args"].append(arg_info) # 戻り値の型アノテーションの抽出 if node.returns: if isinstance(node.returns, ast.Name): function_info["returns"] = node.returns.id elif isinstance(node.returns, ast.Subscript): if isinstance(node.returns.value, ast.Name): function_info["returns"] = f"{node.returns.value.id}[...]" result["functions"].append(function_info) # クラスの抽出 for node in ast.walk(tree): if isinstance(node, ast.ClassDef): class_info = { "name": node.name, "methods": [], "attributes": [], "docstring": ast.get_docstring(node) } # メソッドとクラス属性の抽出 for item in node.body: if isinstance(item, ast.FunctionDef): method_info = { "name": item.name, "args": [], "returns": None, "docstring": ast.get_docstring(item), "is_method": True, "decorators": [d.id if isinstance(d, ast.Name) else "" for d in item.decorator_list] } # メソッド引数の抽出 for arg in item.args.args: if arg.arg != "self" and arg.arg != "cls": arg_info = {"name": arg.arg, "type": None} if arg.annotation: if isinstance(arg.annotation, ast.Name): arg_info["type"] = arg.annotation.id elif isinstance(arg.annotation, ast.Subscript): if isinstance(arg.annotation.value, ast.Name): arg_info["type"] = f"{arg.annotation.value.id}[...]" method_info["args"].append(arg_info) # メソッドの戻り値の型アノテーションの抽出 if item.returns: if isinstance(item.returns, ast.Name): method_info["returns"] = item.returns.id elif isinstance(item.returns, ast.Subscript): if isinstance(item.returns.value, ast.Name): method_info["returns"] = f"{item.returns.value.id}[...]" class_info["methods"].append(method_info) elif isinstance(item, ast.Assign): # クラス属性の抽出 for target in item.targets: if isinstance(target, ast.Name): class_info["attributes"].append(target.id) result["classes"].append(class_info) # コードの複雑さの分析 result["complexity_analysis"] = self._analyze_code_complexity(tree) except SyntaxError as e: print(f"[Error] Python syntax error during code analysis: {str(e)}") except Exception as e: print(f"[Error] Failed to analyze Python code: {str(e)}") def _analyze_javascript_code(self, code: str, result: Dict[str, Any]) -> None: """JavaScriptコードの解析を行う(正規表現ベースの簡易解析)""" # 関数の抽出(正規表現ベース) function_patterns = [ r'function\s+(\w+)\s*\((.*?)\)\s*{', # function name(args) { r'const\s+(\w+)\s*=\s*function\s*\((.*?)\)\s*{', # const name = function(args) { r'const\s+(\w+)\s*=\s*\((.*?)\)\s*=>\s*{', # const name = (args) => { r'const\s+(\w+)\s*=\s*\((.*?)\)\s*=>' # const name = (args) => expression ] for pattern in function_patterns: for match in re.finditer(pattern, code): function_name = match.group(1) args_str = match.group(2).strip() args = [arg.strip() for arg in args_str.split(',') if arg.strip()] function_info = { "name": function_name, "args": [{"name": arg.split('=')[0].strip(), "type": None} for arg in args], "returns": None, "docstring": None, "is_method": False, "decorators": [] } # JSDocコメントの検索 docstring_pattern = r'/\*\*\s*([\s\S]*?)\s*\*/' doc_search_area = code[:match.start()] last_doc = None for doc_match in re.finditer(docstring_pattern, doc_search_area): last_doc = doc_match.group(1) if last_doc: function_info["docstring"] = last_doc result["functions"].append(function_info) # クラスの抽出 class_patterns = [ r'class\s+(\w+)(?:\s+extends\s+(\w+))?\s*{', # class Name [extends Parent] { ] for pattern in class_patterns: for match in re.finditer(pattern, code): class_name = match.group(1) parent_class = match.group(2) if len(match.groups()) > 1 else None class_start = match.end() # クラス本体を取得(中括弧の数を数えて終了位置を見つける) brace_count = 1 class_end = class_start for i in range(class_start, len(code)): if code[i] == '{': brace_count += 1 elif code[i] == '}': brace_count -= 1 if brace_count == 0: class_end = i + 1 break class_body = code[class_start:class_end] # メソッドの抽出 method_pattern = r'(?:async\s+)?(?:static\s+)?(\w+)\s*\((.*?)\)\s*{' methods = [] for method_match in re.finditer(method_pattern, class_body): method_name = method_match.group(1) if method_name not in ['constructor', 'get', 'set']: method_args_str = method_match.group(2).strip() method_args = [arg.strip() for arg in method_args_str.split(',') if arg.strip()] method_info = { "name": method_name, "args": [{"name": arg.split('=')[0].strip(), "type": None} for arg in method_args], "returns": None, "docstring": None, "is_method": True, "decorators": [] } methods.append(method_info) # JSDocコメントの検索 docstring_pattern = r'/\*\*\s*([\s\S]*?)\s*\*/' doc_search_area = code[:match.start()] last_doc = None for doc_match in re.finditer(docstring_pattern, doc_search_area): last_doc = doc_match.group(1) class_info = { "name": class_name, "methods": methods, "attributes": [], # 簡易解析では取得困難 "docstring": last_doc } result["classes"].append(class_info) def _analyze_java_code(self, code: str, result: Dict[str, Any]) -> None: """Javaコードの解析を行う(正規表現ベースの簡易解析)""" # メソッドの抽出 method_pattern = r'(?:public|protected|private|static|\s) +(?:[\w<>\[\]]+\s+)(\w+) *\([^\)]*\) *(?:\{|[^;])' for match in re.finditer(method_pattern, code): method_name = match.group(1) # メソッドのパラメータを抽出 start = match.start() end = match.end() param_area = code[start:end] param_match = re.search(r'\((.*?)\)', param_area) params = [] if param_match: param_str = param_match.group(1).strip() if param_str: param_items = param_str.split(',') for param in param_items: param = param.strip() if param: parts = param.split() if len(parts) >= 2: param_type = parts[0] param_name = parts[1] params.append({"name": param_name, "type": param_type}) # 戻り値の型を抽出 return_match = re.search(r'(?:public|protected|private|static|\s) +([\w<>\[\]]+)\s+' + re.escape(method_name), param_area) return_type = return_match.group(1) if return_match else "void" # メソッドの場合はクラスメソッドに対応するため関数リストには追加しない if "class" in code[:start]: continue function_info = { "name": method_name, "args": params, "returns": return_type, "docstring": None, # 簡易解析のため省略 "is_method": False, "decorators": [] } result["functions"].append(function_info) # クラスの抽出 class_pattern = r'(public|private|protected)?\s*class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?\s*\{' for match in re.finditer(class_pattern, code): class_name = match.group(2) # クラス本体を取得 start = match.end() brace_count = 1 end = start for i in range(start, len(code)): if code[i] == '{': brace_count += 1 elif code[i] == '}': brace_count -= 1 if brace_count == 0: end = i break class_body = code[start:end] # クラスメソッドの抽出 class_methods = [] for method_match in re.finditer(method_pattern, class_body): method_name = method_match.group(1) # メソッドのパラメータを抽出 m_start = method_match.start() m_end = method_match.end() param_area = class_body[m_start:m_end] param_match = re.search(r'\((.*?)\)', param_area) params = [] if param_match: param_str = param_match.group(1).strip() if param_str: param_items = param_str.split(',') for param in param_items: param = param.strip() if param: parts = param.split() if len(parts) >= 2: param_type = parts[0] param_name = parts[1] params.append({"name": param_name, "type": param_type}) # 戻り値の型を抽出 return_match = re.search(r'(?:public|protected|private|static|\s) +([\w<>\[\]]+)\s+' + re.escape(method_name), param_area) return_type = return_match.group(1) if return_match else "void" method_info = { "name": method_name, "args": params, "returns": return_type, "docstring": None, # 簡易解析のため省略 "is_method": True, "decorators": [] } class_methods.append(method_info) # クラス属性の抽出 attribute_pattern = r'(?:public|protected|private|static|\s) +(?!class|void)[\w<>\[\]]+\s+(\w+)\s*;' attributes = [] for attr_match in re.finditer(attribute_pattern, class_body): attr_name = attr_match.group(1) attributes.append(attr_name) class_info = { "name": class_name, "methods": class_methods, "attributes": attributes, "docstring": None # 簡易解析のため省略 } result["classes"].append(class_info) def _analyze_cpp_code(self, code: str, result: Dict[str, Any]) -> None: """C++コードの解析を行う(正規表現ベースの簡易解析)""" # 関数の抽出 function_pattern = r'(?!if|for|while|switch)\b(\w+(?:<[\w\s,]+>)?)\s+(\w+)\s*\((.*?)\)\s*(?:const)?\s*(?:noexcept)?\s*(?:=\s*delete)?\s*(?:=\s*default)?\s*(?:override)?\s*(?:final)?\s*(?:{\|;)' for match in re.finditer(function_pattern, code): return_type = match.group(1) function_name = match.group(2) params_str = match.group(3).strip() # クラスメソッドかどうかを判定 preceding_code = code[:match.start()] class_definition = re.search(r'class\s+\w+\s*(?::\s*(?:public|protected|private)\s*\w+(?:\s*,\s*(?:public|protected|private)\s*\w+)*)?\s*{', preceding_code) if class_definition: # クラス定義内の関数はクラスメソッドとして処理 struct_end = preceding_code.rfind('}') if struct_end == -1 or struct_end < class_definition.start(): continue # クラス内のメソッドはスキップ # パラメータの解析 params = [] if params_str: param_list = [] paren_level = 0 current_param = "" for char in params_str: if char == ',' and paren_level == 0: param_list.append(current_param.strip()) current_param = "" else: if char == '(': paren_level += 1 elif char == ')': paren_level -= 1 current_param += char if current_param: param_list.append(current_param.strip()) for param in param_list: param_parts = param.split() if len(param_parts) >= 2: param_type = ' '.join(param_parts[:-1]) param_name = param_parts[-1].replace('&', '').replace('*', '') params.append({"name": param_name, "type": param_type}) function_info = { "name": function_name, "args": params, "returns": return_type, "docstring": None, # 簡易解析のため省略 "is_method": False, "decorators": [] } result["functions"].append(function_info) # クラスの抽出 class_pattern = r'(?:class|struct)\s+(\w+)(?:\s*:\s*(?:public|protected|private)\s*(\w+)(?:\s*,\s*(?:public|protected|private)\s*\w+)*)?\s*\{' for match in re.finditer(class_pattern, code): class_name = match.group(1) # クラス本体を取得 start = match.end() brace_count = 1 end = start for i in range(start, len(code)): if code[i] == '{': brace_count += 1 elif code[i] == '}': brace_count -= 1 if brace_count == 0: end = i break class_body = code[start:end] # クラスメソッドの抽出 method_pattern = r'(?:public|protected|private|virtual|static|\s)*\s+(\w+(?:<[\w\s,]+>)?)\s+(\w+)\s*\((.*?)\)\s*(?:const)?\s*(?:noexcept)?\s*(?:=\s*delete)?\s*(?:=\s*default)?\s*(?:override)?\s*(?:final)?\s*(?:{\|;)' class_methods = [] for method_match in re.finditer(method_pattern, class_body): return_type = method_match.group(1) method_name = method_match.group(2) if method_name == class_name or method_name == f"~{class_name}": continue # コンストラクタとデストラクタは除外 params_str = method_match.group(3).strip() # パラメータの解析 params = [] if params_str: param_list = [] paren_level = 0 current_param = "" for char in params_str: if char == ',' and paren_level == 0: param_list.append(current_param.strip()) current_param = "" else: if char == '(': paren_level += 1 elif char == ')': paren_level -= 1 current_param += char if current_param: param_list.append(current_param.strip()) for param in param_list: param_parts = param.split() if len(param_parts) >= 2: param_type = ' '.join(param_parts[:-1]) param_name = param_parts[-1].replace('&', '').replace('*', '') params.append({"name": param_name, "type": param_type}) method_info = { "name": method_name, "args": params, "returns": return_type, "docstring": None, # 簡易解析のため省略 "is_method": True, "decorators": [] } class_methods.append(method_info) # クラス属性の抽出 attribute_pattern = r'(?:public|protected|private|static|\s)*\s+(?!class|struct|void|return|if|for|while)[\w<>\[\]]+\s+(\w+)\s*;' attributes = [] for attr_match in re.finditer(attribute_pattern, class_body): attr_name = attr_match.group(1) attributes.append(attr_name) class_info = { "name": class_name, "methods": class_methods, "attributes": attributes, "docstring": None # 簡易解析のため省略 } result["classes"].append(class_info) def _analyze_code_complexity(self, ast_tree) -> Dict[str, Any]: """コードの複雑さを分析する""" complexity = { "cyclomatic_complexity": 0, "nested_blocks": 0, "max_nesting_level": 0, "line_count": 0 } # 循環的複雑度の計算 current_nesting_level = 0 max_nesting_level = 0 for node in ast.walk(ast_tree): # if, for, while, try, withステートメントの計数 if isinstance(node, (ast.If, ast.For, ast.While, ast.Try, ast.With)): complexity["cyclomatic_complexity"] += 1 current_nesting_level += 1 max_nesting_level = max(max_nesting_level, current_nesting_level) complexity["nested_blocks"] += 1 # ネスティングレベルの調整 if hasattr(node, 'body') and isinstance(node.body, list) and node.body: if isinstance(node, (ast.FunctionDef, ast.ClassDef)): # 関数とクラスの開始ではネスティングレベルをリセット current_nesting_level = 0 complexity["max_nesting_level"] = max_nesting_level return complexity def _identify_edge_cases_and_patterns(self, code: str, result: Dict[str, Any], language: str) -> None: """エッジケースとパターンを特定する""" # 重要なパターンとそれに対応するエッジケース patterns = { "division": (r'/\s*\w+', "ゼロ除算"), "array_access": (r'\[\s*\w+\s*\]', "配列の範囲外アクセス"), "null_check": (r'==\s*null|!=\s*null|==\s*None|!=\s*None', "NullPointerException/TypeError"), "file_operation": (r'open\s*\(|fopen\s*\(|readFile|writeFile', "ファイル操作エラー"), "network_request": (r'http|socket|fetch|request', "ネットワークエラー、タイムアウト"), "database_query": (r'query|sql|select|insert|update|delete|from\s+\w+', "データベースエラー"), "recursion": (r'def\s+(\w+).*\1\s*\(|function\s+(\w+).*\2\s*\(', "スタックオーバーフロー"), "float_comparison": (r'==\s*\d+\.\d+|!=\s*\d+\.\d+', "浮動小数点の比較精度問題"), "memory_allocation": (r'malloc|new\s+\w+|new\s+\[', "メモリ割り当て失敗"), "concurrent_access": (r'thread|mutex|lock|atomic|synchronized', "競合状態"), "user_input": (r'input|scanf|readline|gets', "不正な入力形式"), "physics_simulation": (r'force|acceleration|velocity|time\s*step|dt', "数値安定性の問題、精度の問題") } # 物理シミュレーション固有のパターン physics_patterns = { "time_integration": (r'dt|time\s*step|delta\s*t', "タイムステップの精度と安定性"), "boundary_condition": (r'boundary|edge|border', "境界条件の処理"), "collision_detection": (r'collision|intersect|overlap', "衝突検出の精度と効率"), "energy_conservation": (r'energy|conservation|保存', "エネルギー保存則の検証"), "floating_point_precision": (r'float|double|decimal', "浮動小数点の精度問題"), "vector_operations": (r'vector|cross|dot|normalize', "ベクトル演算の精度と効率"), "matrix_operations": (r'matrix|determinant|inverse', "行列演算の精度と安定性"), "numerical_stability": (r'stable|unstable|cfl|courant', "数値安定性の問題") } # 言語固有のパターン language_specific_patterns = { "python": { "list_comprehension": (r'\[\s*\w+\s+for\s+\w+\s+in', "大きなリストの処理効率"), "generator": (r'yield\s+\w+', "ジェネレータの再開可能性"), "context_manager": (r'with\s+\w+', "コンテキストマネージャのエラー処理") }, "javascript": { "async_await": (r'async|await', "非同期処理の例外処理"), "promise": (r'Promise|then|catch', "未処理のPromise拒否"), "event_listener": (r'addEventListener|on\w+\s*=', "イベントリスナの漏れ") }, "java": { "exception_handling": (r'try|catch|throws', "例外の伝播と処理"), "generics": (r'<\w+>', "型安全性と型消去"), "multi_threading": (r'Thread|Runnable|Callable', "スレッドセーフティ") }, "cpp": { "memory_management": (r'delete|free', "メモリリーク、二重解放"), "move_semantics": (r'std::move', "ムーブ後のオブジェクト状態"), "templates": (r'template\s*<', "テンプレート特殊化の選択") } } # パターンの検出 for pattern_name, (regex, edge_case) in patterns.items(): if re.search(regex, code, re.IGNORECASE): result["identified_patterns"].append(pattern_name) result["edge_cases"].append(edge_case) # 物理シミュレーション固有のパターン検出 for pattern_name, (regex, edge_case) in physics_patterns.items(): if re.search(regex, code, re.IGNORECASE): result["identified_patterns"].append(f"physics_{pattern_name}") result["edge_cases"].append(edge_case) # 言語固有のパターン検出 if language in language_specific_patterns: for pattern_name, (regex, edge_case) in language_specific_patterns[language].items(): if re.search(regex, code, re.IGNORECASE): result["identified_patterns"].append(f"{language}_{pattern_name}") result["edge_cases"].append(edge_case) def _build_test_generation_prompt(self, code: str, test_analysis: Dict[str, Any], requirements: str = None, language: str = "python") -> str: """テスト生成用のプロンプトを構築する""" functions_str = "" for func in test_analysis["functions"]: args_str = ", ".join([f"{arg['name']}: {arg['type'] or 'Any'}" for arg in func["args"]]) returns_str = f" -> {func['returns']}" if func["returns"] else "" functions_str += f"- {func['name']}({args_str}){returns_str}\n" if func["docstring"]: functions_str += f" Docstring: {func['docstring']}\n" classes_str = "" for cls in test_analysis["classes"]: classes_str += f"- Class: {cls['name']}\n" if cls["docstring"]: classes_str += f" Docstring: {cls['docstring']}\n" if cls["attributes"]: classes_str += f" Attributes: {', '.join(cls['attributes'])}\n" if cls["methods"]: classes_str += " Methods:\n" for method in cls["methods"]: args_str = ", ".join([f"{arg['name']}: {arg['type'] or 'Any'}" for arg in method["args"]]) returns_str = f" -> {method['returns']}" if method['returns'] else "" classes_str += f" - {method['name']}({args_str}){returns_str}\n" if method["docstring"]: classes_str += f" Docstring: {method['docstring']}\n" edge_cases_str = "\n".join([f"- {case}" for case in test_analysis["edge_cases"]]) patterns_str = "\n".join([f"- {pattern}" for pattern in test_analysis["identified_patterns"]]) # テスト生成用プロンプトの構築 prompt = f"""あなたは高品質なテストコードの専門家です。以下のコードに対して包括的なテストを生成してください。 ## コードの概要 ```{language} {code} ``` ## コード解析結果 関数: {functions_str or "なし"} クラス: {classes_str or "なし"} 考慮すべきエッジケース: {edge_cases_str or "特になし"} 識別されたパターン: {patterns_str or "特になし"} """ # 要件が提供されている場合は追加 if requirements: prompt += f""" ## 機能要件 {requirements} """ prompt += f""" ## テスト要件 1. 全ての公開関数とメソッドのテストを作成してください 2. 基本的な機能テストだけでなく、境界値と異常系のテストも含めてください 3. モック/スタブを適切に使用して外部依存関係を排除してください 4. テストの可読性と保守性を確保してください 5. テストカバレッジを最大化してください 6. 設計によって特定されたエッジケースを確実に検証してください 7. 物理シミュレーションの場合は、数値的安定性と物理法則の保存をテストすること ## 必須テストケース """ # エッジケースに基づいた必須テストケースを追加 for edge_case in test_analysis["edge_cases"]: prompt += f"- {edge_case}に対するテスト\n" # 物理シミュレーション関連のパターンがある場合 if any("physics_" in pattern for pattern in test_analysis["identified_patterns"]): prompt += """ ## 物理シミュレーション向け特別テスト 1. 保存則(エネルギー、運動量など)の検証テスト 2. 極端な入力値での安定性テスト 3. 長時間シミュレーションでの安定性テスト 4. 既知の解析解との比較テスト 5. 数値精度の検証テスト """ # 言語別のテストフレームワーク指定 test_frameworks = { "python": "unittest または pytest", "javascript": "Jest または Mocha", "java": "JUnit", "cpp": "Google Test または Catch2" } framework = test_frameworks.get(language, "適切なテストフレームワーク") prompt += f""" ## 出力形式 {framework}を使用したテストコードを生成してください。テストコードには以下を含めてください: 1. 適切なセットアップとティアダウン 2. 明確なテスト名と説明(テストが何をテストしているかを示す) 3. 予想される結果とその理由の説明 4. エッジケースのテスト 5. 異常系のテスト(不正な入力、例外処理など) テストコードは実行可能で、元のコードに対して直接実行できるようにしてください。 """ return prompt