testgenie-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. testgenie_py-0.1.0/PKG-INFO +24 -0
  2. testgenie_py-0.1.0/README.md +0 -0
  3. testgenie_py-0.1.0/pyproject.toml +28 -0
  4. testgenie_py-0.1.0/testgen/__init__.py +0 -0
  5. testgenie_py-0.1.0/testgen/analyzer/__init__.py +0 -0
  6. testgenie_py-0.1.0/testgen/analyzer/ast_analyzer.py +149 -0
  7. testgenie_py-0.1.0/testgen/analyzer/contracts/__init__.py +0 -0
  8. testgenie_py-0.1.0/testgen/analyzer/contracts/contract.py +13 -0
  9. testgenie_py-0.1.0/testgen/analyzer/contracts/no_exception_contract.py +16 -0
  10. testgenie_py-0.1.0/testgen/analyzer/contracts/nonnull_contract.py +15 -0
  11. testgenie_py-0.1.0/testgen/analyzer/fuzz_analyzer.py +106 -0
  12. testgenie_py-0.1.0/testgen/analyzer/random_feedback_analyzer.py +291 -0
  13. testgenie_py-0.1.0/testgen/analyzer/reinforcement_analyzer.py +75 -0
  14. testgenie_py-0.1.0/testgen/analyzer/test_case_analyzer.py +46 -0
  15. testgenie_py-0.1.0/testgen/analyzer/test_case_analyzer_context.py +58 -0
  16. testgenie_py-0.1.0/testgen/controller/__init__.py +0 -0
  17. testgenie_py-0.1.0/testgen/controller/cli_controller.py +194 -0
  18. testgenie_py-0.1.0/testgen/controller/docker_controller.py +169 -0
  19. testgenie_py-0.1.0/testgen/docker/Dockerfile +22 -0
  20. testgenie_py-0.1.0/testgen/docker/poetry.lock +361 -0
  21. testgenie_py-0.1.0/testgen/docker/pyproject.toml +22 -0
  22. testgenie_py-0.1.0/testgen/generator/__init__.py +0 -0
  23. testgenie_py-0.1.0/testgen/generator/code_generator.py +66 -0
  24. testgenie_py-0.1.0/testgen/generator/doctest_generator.py +208 -0
  25. testgenie_py-0.1.0/testgen/generator/generator.py +55 -0
  26. testgenie_py-0.1.0/testgen/generator/pytest_generator.py +77 -0
  27. testgenie_py-0.1.0/testgen/generator/test_generator.py +26 -0
  28. testgenie_py-0.1.0/testgen/generator/unit_test_generator.py +84 -0
  29. testgenie_py-0.1.0/testgen/inspector/__init__.py +0 -0
  30. testgenie_py-0.1.0/testgen/inspector/inspector.py +61 -0
  31. testgenie_py-0.1.0/testgen/main.py +13 -0
  32. testgenie_py-0.1.0/testgen/models/__init__.py +0 -0
  33. testgenie_py-0.1.0/testgen/models/analysis_context.py +56 -0
  34. testgenie_py-0.1.0/testgen/models/function_metadata.py +61 -0
  35. testgenie_py-0.1.0/testgen/models/generator_context.py +63 -0
  36. testgenie_py-0.1.0/testgen/models/test_case.py +8 -0
  37. testgenie_py-0.1.0/testgen/presentation/__init__.py +0 -0
  38. testgenie_py-0.1.0/testgen/presentation/cli_view.py +12 -0
  39. testgenie_py-0.1.0/testgen/q_table/global_q_table.json +1 -0
  40. testgenie_py-0.1.0/testgen/reinforcement/__init__.py +0 -0
  41. testgenie_py-0.1.0/testgen/reinforcement/abstract_state.py +7 -0
  42. testgenie_py-0.1.0/testgen/reinforcement/agent.py +153 -0
  43. testgenie_py-0.1.0/testgen/reinforcement/environment.py +215 -0
  44. testgenie_py-0.1.0/testgen/reinforcement/statement_coverage_state.py +33 -0
  45. testgenie_py-0.1.0/testgen/service/__init__.py +0 -0
  46. testgenie_py-0.1.0/testgen/service/analysis_service.py +260 -0
  47. testgenie_py-0.1.0/testgen/service/cfg_service.py +55 -0
  48. testgenie_py-0.1.0/testgen/service/generator_service.py +169 -0
  49. testgenie_py-0.1.0/testgen/service/service.py +389 -0
  50. testgenie_py-0.1.0/testgen/sqlite/__init__.py +0 -0
  51. testgenie_py-0.1.0/testgen/sqlite/db.py +84 -0
  52. testgenie_py-0.1.0/testgen/sqlite/db_service.py +219 -0
  53. testgenie_py-0.1.0/testgen/tree/__init__.py +0 -0
  54. testgenie_py-0.1.0/testgen/tree/node.py +7 -0
  55. testgenie_py-0.1.0/testgen/tree/tree_utils.py +79 -0
  56. testgenie_py-0.1.0/testgen/util/__init__.py +0 -0
  57. testgenie_py-0.1.0/testgen/util/coverage_utils.py +168 -0
  58. testgenie_py-0.1.0/testgen/util/coverage_visualizer.py +154 -0
  59. testgenie_py-0.1.0/testgen/util/file_utils.py +110 -0
  60. testgenie_py-0.1.0/testgen/util/randomizer.py +122 -0
  61. testgenie_py-0.1.0/testgen/util/utils.py +143 -0
  62. testgenie_py-0.1.0/testgen/util/z3_utils/__init__.py +0 -0
  63. testgenie_py-0.1.0/testgen/util/z3_utils/ast_to_z3.py +99 -0
  64. testgenie_py-0.1.0/testgen/util/z3_utils/branch_condition.py +72 -0
  65. testgenie_py-0.1.0/testgen/util/z3_utils/constraint_extractor.py +36 -0
  66. testgenie_py-0.1.0/testgen/util/z3_utils/variable_finder.py +10 -0
  67. testgenie_py-0.1.0/testgen/util/z3_utils/z3_test_case.py +94 -0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.3
2
+ Name: testgenie-py
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: cjseitz
6
+ Author-email: charlesjseitz@gmail.com
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: ConfigArgParse (==1.7)
14
+ Requires-Dist: astor (==0.8.1)
15
+ Requires-Dist: atheris (==2.3.0)
16
+ Requires-Dist: coverage (==7.6.4)
17
+ Requires-Dist: klara (==0.6.3)
18
+ Requires-Dist: pytest (>=8.3.5,<9.0.0)
19
+ Requires-Dist: staticfg (>=0.9.5,<0.10.0)
20
+ Requires-Dist: typed-ast (==1.5.5)
21
+ Requires-Dist: z3-solver (==4.13.3.0)
22
+ Description-Content-Type: text/markdown
23
+
24
+
File without changes
@@ -0,0 +1,28 @@
1
+ [tool.poetry]
2
+ name = "testgenie-py"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["cjseitz <charlesjseitz@gmail.com>"]
6
+ readme = "README.md"
7
+
8
+ [[tool.poetry.packages]]
9
+ include = "testgen"
10
+
11
+ [tool.poetry.scripts]
12
+ testgenie = "main:main"
13
+
14
+ [tool.poetry.dependencies]
15
+ python = "^3.10"
16
+ astor = "0.8.1"
17
+ atheris = "2.3.0"
18
+ ConfigArgParse = "1.7"
19
+ coverage = "7.6.4"
20
+ klara = "0.6.3"
21
+ typed-ast = "1.5.5"
22
+ z3-solver = "4.13.3.0"
23
+ staticfg = "^0.9.5"
24
+ pytest = "^8.3.5"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core"]
28
+ build-backend = "poetry.core.masonry.api"
File without changes
File without changes
@@ -0,0 +1,149 @@
1
+ import ast
2
+ from typing import List
3
+
4
+ import testgen.util.file_utils
5
+ from testgen.models.test_case import TestCase
6
+ from testgen.analyzer.test_case_analyzer import TestCaseAnalyzerStrategy
7
+ from abc import ABC
8
+ from testgen.models.function_metadata import FunctionMetadata
9
+
10
+ class ASTAnalyzer(TestCaseAnalyzerStrategy, ABC):
11
+ def collect_test_cases(self, function_metadata: FunctionMetadata) -> List[TestCase]:
12
+ """Collect test cases by analyzing AST conditions and return statements"""
13
+
14
+ if function_metadata and function_metadata.params:
15
+ param_names = list(function_metadata.params.keys())
16
+ else:
17
+ param_names = [arg.arg for arg in function_metadata.func_def.args.args
18
+ if arg.arg != 'self']
19
+
20
+ test_cases: List[TestCase] = self.get_conditions_recursively(
21
+ function_metadata,
22
+ function_metadata.function_name,
23
+ function_metadata.func_def.body,
24
+ param_names)
25
+ return test_cases
26
+
27
+ def get_conditions_recursively(self, function_metadata: FunctionMetadata, func_name: str, func_node_body: list, param_names, test_cases=None, conditions=None) -> List[TestCase]:
28
+ if conditions is None:
29
+ conditions = []
30
+
31
+ if test_cases is None:
32
+ test_cases = []
33
+
34
+ for node in func_node_body:
35
+ if isinstance(node, ast.If):
36
+ condition_str = self.parse_condition(node.test)
37
+ print(f"Condition found in function: {condition_str}")
38
+ self.get_conditions_recursively(function_metadata, func_name, node.body, param_names, test_cases, conditions + [condition_str])
39
+ if node.orelse:
40
+ self.get_conditions_recursively(function_metadata, func_name, node.orelse, param_names, test_cases, conditions)
41
+
42
+ elif isinstance(node, ast.Return):
43
+ inputs = self.generate_inputs_from_conditions(conditions, param_names)
44
+
45
+ input_exists = False
46
+ for existing_test in test_cases:
47
+ if existing_test.func_name == func_name and existing_test.inputs == inputs:
48
+ input_exists = True
49
+ break
50
+
51
+ if not input_exists:
52
+ try:
53
+ generated_file = function_metadata.filename
54
+ module = testgen.util.file_utils.load_module(generated_file)
55
+
56
+ # Check if we're dealing with a class
57
+ if function_metadata.class_name:
58
+ cls = getattr(module, function_metadata.class_name)
59
+ instance = cls()
60
+ func = getattr(instance, func_name)
61
+ output = func(*inputs)
62
+ else:
63
+ func = getattr(module, func_name)
64
+ output = func(*inputs)
65
+
66
+ except Exception as e:
67
+ print(f"Error executing function: {e}")
68
+ output = self._try_to_determine_output(node)
69
+
70
+ test_case = TestCase(func_name, inputs, output)
71
+ test_cases.append(test_case)
72
+
73
+ return test_cases
74
+
75
+ def _try_to_determine_output(self, return_node):
76
+ """Try different strategies to determine the output value"""
77
+ try:
78
+ # Try direct evaluation
79
+ return ast.literal_eval(return_node.value)
80
+ except (ValueError, SyntaxError):
81
+ # Check for constants
82
+ if isinstance(return_node.value, ast.Constant):
83
+ return return_node.value.value
84
+ # Check for name values
85
+ elif isinstance(return_node.value, ast.Name):
86
+ if return_node.value.id == 'True':
87
+ return True
88
+ elif return_node.value.id == 'False':
89
+ return False
90
+ # Default fallback
91
+ return None
92
+
93
+ @staticmethod
94
+ def parse_condition(condition_node):
95
+ """Parse an AST condition node into a string representation."""
96
+ try:
97
+ if isinstance(condition_node, ast.Compare):
98
+ left = condition_node.left.id if isinstance(condition_node.left, ast.Name) else str(condition_node.left)
99
+ op = type(condition_node.ops[0]).__name__
100
+ right = condition_node.comparators[0].value if isinstance(condition_node.comparators[0], ast.Constant) else str(condition_node.comparators[0])
101
+ return f"{left} {op} {right}"
102
+ elif isinstance(condition_node, ast.BoolOp):
103
+ # Use a simple representation for boolean operations
104
+ return f"BoolOp({type(condition_node.op).__name__})"
105
+ elif isinstance(condition_node, ast.UnaryOp):
106
+ # Use a simple representation for unary operations
107
+ return f"UnaryOp({type(condition_node.op).__name__})"
108
+ elif isinstance(condition_node, ast.Name):
109
+ return condition_node.id
110
+ elif isinstance(condition_node, ast.Constant):
111
+ return str(condition_node.value)
112
+
113
+ return ast.dump(condition_node)
114
+ except Exception as e:
115
+ print(f"Error parsing condition: {e}")
116
+ return "UnknownCondition"
117
+
118
+ @staticmethod
119
+ def generate_inputs_from_conditions(conditions, param_names):
120
+ inputs = []
121
+
122
+ # Create parameter values with more variety based on condition path
123
+ param_values = {}
124
+
125
+ # If no meaningful conditions were found, create varied inputs
126
+ if not any(param in cond for cond in conditions for param in param_names):
127
+ for i, param in enumerate(param_names):
128
+ # Alternate True/False to create diverse test cases
129
+ param_values[param] = (i % 2 == 0)
130
+ else:
131
+ # Start with all False
132
+ param_values = {param: False for param in param_names}
133
+
134
+ # Process each condition to extract parameter values
135
+ for cond in conditions:
136
+ for param in param_names:
137
+ if param in cond:
138
+ if "Eq True" in cond or "== True" in cond:
139
+ param_values[param] = True
140
+ elif "Eq False" in cond or "== False" in cond:
141
+ param_values[param] = False
142
+ elif f"Not({param})" in cond:
143
+ param_values[param] = False
144
+
145
+ # Build the input tuple
146
+ for param in param_names:
147
+ inputs.append(param_values[param])
148
+
149
+ return tuple(inputs)
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class Contract(ABC):
4
+ @abstractmethod
5
+ def check_preconditions(self, args) -> bool:
6
+ """Check preconditions before the method executes."""
7
+ pass
8
+
9
+ @abstractmethod
10
+ def check_postconditions(self, args, output, exception) -> bool:
11
+ """Check postconditions before the method executes."""
12
+ pass
13
+
@@ -0,0 +1,16 @@
1
+ from testgen.analyzer.contracts.contract import Contract
2
+ from abc import ABC
3
+
4
+ class NoExceptionContract(Contract, ABC):
5
+ """Ensures function does not raise an exception"""
6
+
7
+ def check_preconditions(self, args) -> bool:
8
+ """Returns False if there is a violation"""
9
+ return True
10
+
11
+ """Returns False if there is an exception"""
12
+ def check_postconditions(self, args, output, exception) -> bool:
13
+ if exception is not None:
14
+ return True
15
+ else:
16
+ return False
@@ -0,0 +1,15 @@
1
+ from abc import ABC
2
+
3
+ from testgen.analyzer.contracts.contract import Contract
4
+
5
+ class NonNullContract(Contract, ABC):
6
+ """Ensures the function does not contain null or None input values."""
7
+ def check_preconditions(self, args) -> bool:
8
+ return all(arg is not None for arg in args)
9
+
10
+ """Ensures the return value is not None"""
11
+ def check_postconditions(self, args, output, exception) -> bool:
12
+ if exception:
13
+ return False
14
+ else:
15
+ return output is not None
@@ -0,0 +1,106 @@
1
+ from abc import ABC
2
+ import os
3
+ import traceback
4
+ from typing import List
5
+
6
+ from atheris.native import FuzzedDataProvider
7
+
8
+ import coverage
9
+
10
+ from testgen.models.test_case import TestCase
11
+ from testgen.analyzer.test_case_analyzer import TestCaseAnalyzerStrategy
12
+
13
+ from testgen.models.function_metadata import FunctionMetadata
14
+
15
+ class FuzzAnalyzer(TestCaseAnalyzerStrategy, ABC):
16
+
17
+ def __init__(self, analysis_context=None):
18
+ super().__init__(analysis_context)
19
+ self.coverage_tracker = coverage.Coverage(branch=True)
20
+ self.executed_branches = set()
21
+
22
+ # TODO: Use getattr() to get function in FunctionMetadata
23
+ def collect_test_cases(self, function_metadata: FunctionMetadata) -> List[TestCase]:
24
+ """Collect test cases using fuzzing techniques"""
25
+
26
+ # Use module from function_metadata if available
27
+ if function_metadata and function_metadata.module:
28
+ module = self.analysis_context.module
29
+ else:
30
+ raise ValueError("Module not set in function metadata. Cannot perform fuzzing without a module.")
31
+
32
+ class_name = function_metadata.class_name if function_metadata.class_name else None
33
+ try:
34
+ if not class_name is None:
35
+ cls = getattr(module, class_name, None)
36
+ func = getattr(cls(), function_metadata.function_name, None) if cls else None
37
+ else:
38
+ func = getattr(module, function_metadata.function_name, None)
39
+ if func:
40
+ return self.run_fuzzing(func, function_metadata.function_name, function_metadata.params, module, 10)
41
+ except Exception as e:
42
+ print(f"[FUZZ ANALYZER ERROR]: {e}")
43
+ traceback.print_exc()
44
+ return []
45
+
46
+ def run_fuzzing(self, func, func_name, param_types, module, iterations=10) -> List[TestCase]:
47
+ """Run the function with fuzzed inputs and collect failing test cases."""
48
+ print(f"Running fuzzing {func_name}")
49
+ test_cases = []
50
+
51
+ for _ in range(iterations):
52
+ fdp = FuzzedDataProvider(os.urandom(1024))
53
+ inputs = self.generate_inputs_from_fuzz_data(fdp, param_types)
54
+
55
+ self.coverage_tracker.erase()
56
+ self.coverage_tracker.start()
57
+
58
+ try:
59
+ output = func(*inputs)
60
+ self.coverage_tracker.stop()
61
+ covered_branches = self.get_branch_coverage(module)
62
+ covered_branches_tuple = tuple(covered_branches)
63
+
64
+ print(f"[COVERED_BRANCHES]: {covered_branches}")
65
+ print(f"[EXECUTED BRANCHES]: {self.executed_branches}")
66
+
67
+ for branch in covered_branches:
68
+ if branch[1] < 0:
69
+ if covered_branches_tuple not in self.executed_branches:
70
+ self.executed_branches.add(covered_branches_tuple)
71
+ test_cases.append(TestCase(func_name, inputs, output))
72
+
73
+ except Exception as e:
74
+ self.coverage_tracker.stop()
75
+ test_cases.append(TestCase(func_name, inputs, (type(e), str(e) + " EXCEPTION")))
76
+ self.executed_branches.clear()
77
+ return test_cases
78
+
79
+
80
+ # TODO: Look into FuzzDataProvider.instrumentall()
81
+ @staticmethod
82
+ def generate_inputs_from_fuzz_data(fdp: FuzzedDataProvider, param_types):
83
+ """Generate fuzzed inputs based on parameter types."""
84
+ inputs = []
85
+ for param_type in param_types.values():
86
+ if param_type == "int":
87
+ inputs.append(fdp.ConsumeInt(4))
88
+ elif param_type == "bool":
89
+ inputs.append(fdp.ConsumeBool())
90
+ elif param_type == "float":
91
+ inputs.append(fdp.ConsumeFloat())
92
+ elif param_type == "str":
93
+ inputs.append(fdp.ConsumeString(10))
94
+ elif param_type == "bytes":
95
+ inputs.append(fdp.ConsumeBytes(10))
96
+ else:
97
+ inputs.append(None)
98
+ return tuple(inputs)
99
+
100
+ def get_branch_coverage(self, module):
101
+ data = self.coverage_tracker.get_data()
102
+ try:
103
+ return data.arcs(module.__file__)
104
+ except Exception as e:
105
+ print(f"[ERROR: FUZZ ANALYZER] Couldn't get arcs Exception: {e}")
106
+ return set()
@@ -0,0 +1,291 @@
1
+ import ast
2
+ import importlib
3
+ import random
4
+ import time
5
+ import traceback
6
+ from typing import List, Dict, Set
7
+
8
+ import testgen.util.randomizer
9
+ import testgen.util.utils as utils
10
+ import testgen.util.coverage_utils as coverage_utils
11
+ from testgen.analyzer.contracts.contract import Contract
12
+ from testgen.analyzer.contracts.no_exception_contract import NoExceptionContract
13
+ from testgen.analyzer.contracts.nonnull_contract import NonNullContract
14
+ from testgen.models.test_case import TestCase
15
+ from testgen.analyzer.test_case_analyzer import TestCaseAnalyzerStrategy
16
+ from abc import ABC
17
+
18
+ from testgen.models.function_metadata import FunctionMetadata
19
+
20
+
21
+ # Citation in which this method and algorithm were taken from:
22
+ # C. Pacheco, S. K. Lahiri, M. D. Ernst and T. Ball, "Feedback-Directed Random Test Generation," 29th International
23
+ # Conference on Software Engineering (ICSE'07), Minneapolis, MN, USA, 2007, pp. 75-84, doi: 10.1109/ICSE.2007.37.
24
+ # keywords: {System testing;Contracts;Object oriented modeling;Law;Legal factors;Open source software;Software
25
+ # testing;Feedback;Filters;Error correction codes},
26
+
27
+ class RandomFeedbackAnalyzer(TestCaseAnalyzerStrategy, ABC):
28
+ def __init__(self, analysis_context=None):
29
+ super().__init__(analysis_context)
30
+ self.test_cases = []
31
+ self.covered_lines: Dict[str, Set[int]] = {}
32
+
33
+ # Algorithm described in above article
34
+ # Classes is the classes for which we want to generate sequences
35
+ # Contracts express invariant properties that hold both at entry and exit from a call
36
+ # Contract takes as input the current state of the system (runtime values created in the sequence so far, and any exception thrown by the last call), and returns satisfied or violated
37
+ # Output is the runtime values and boolean flag violated
38
+ # Filters determine which values of a sequence are extensible and should be used as inputs
39
+ def generate_sequences(self, function_metadata: List[FunctionMetadata], classes=None, contracts: List[Contract] = None, filters=None, time_limit=20):
40
+ contracts = [NonNullContract(), NoExceptionContract()]
41
+ error_seqs = [] # execution violates a contract
42
+ non_error_seqs = [] # execution does not violate a contract
43
+
44
+ functions = self._analysis_context.function_data
45
+ start_time = time.time()
46
+ while(time.time() - start_time) >= time_limit:
47
+ # Get random function
48
+ func = random.choice(functions)
49
+ param_types: dict = func.params
50
+ vals: dict = self.random_seqs_and_vals(param_types)
51
+ new_seq = (func.function_name, vals)
52
+ if new_seq in error_seqs or new_seq in non_error_seqs:
53
+ continue
54
+ outs_violated: tuple = self.execute_sequence(new_seq, contracts)
55
+ violated: bool = outs_violated[1]
56
+ # Create tuple of sequence ((func name, args), output)
57
+ new_seq_out = (new_seq, outs_violated[0])
58
+ if violated:
59
+ error_seqs.append(new_seq_out)
60
+ else:
61
+ # Question: Should I use the failed contract to be the assertion in unit test??
62
+ non_error_seqs.append(new_seq_out)
63
+ return error_seqs, non_error_seqs
64
+
65
+ def generate_sequences_new(self, contracts: List[Contract] = None, filters=None, time_limit=20):
66
+ contracts = [NonNullContract(), NoExceptionContract()]
67
+ error_seqs = [] # execution violates a contract
68
+ non_error_seqs = [] # execution does not violate a contract
69
+
70
+ functions = self._analysis_context.function_data.copy()
71
+ start_time = time.time()
72
+
73
+ while (time.time() - start_time) < time_limit:
74
+ # Get random function
75
+ func = random.choice(functions)
76
+ param_types: dict = func.params
77
+ vals: dict = self.random_seqs_and_vals(param_types)
78
+ new_seq = (func.function_name, vals)
79
+
80
+ if new_seq in [seq[0] for seq in error_seqs] or new_seq in [seq[0] for seq in non_error_seqs]:
81
+ continue
82
+
83
+ outs_violated: tuple = self.execute_sequence(new_seq, contracts)
84
+ violated: bool = outs_violated[1]
85
+
86
+ # Create tuple of sequence ((func name, args), output)
87
+ new_seq_out = (new_seq, outs_violated[0])
88
+
89
+ if violated:
90
+ error_seqs.append(new_seq_out)
91
+
92
+ else:
93
+ non_error_seqs.append(new_seq_out)
94
+
95
+ test_case = TestCase(new_seq_out[0][0], tuple(new_seq_out[0][1].values()), new_seq_out[1])
96
+ self.test_cases.append(test_case)
97
+ fully_covered = self.covered(func)
98
+ if fully_covered:
99
+ print(f"Function {func.function_name} is fully covered")
100
+ functions.remove(func)
101
+
102
+ if not functions:
103
+ self.test_cases.sort(key=lambda tc: tc.func_name)
104
+ print("All functions covered")
105
+ break
106
+
107
+ self.test_cases.sort(key=lambda tc: tc.func_name)
108
+ return error_seqs, non_error_seqs
109
+
110
+
111
+ def covered(self, func: FunctionMetadata) -> bool:
112
+ if func.function_name not in self.covered_lines:
113
+ self.covered_lines[func.function_name] = set()
114
+
115
+ for test_case in [tc for tc in self.test_cases if tc.func_name == func.function_name]:
116
+ analysis = coverage_utils.get_coverage_analysis(self._analysis_context.filepath,
117
+ func.function_name, test_case.inputs)
118
+ covered = coverage_utils.get_list_of_covered_statements(analysis)
119
+ self.covered_lines[func.function_name].update(covered)
120
+
121
+ executable_statements = self.get_all_executable_statements(func)
122
+
123
+ return self.covered_lines[func.function_name] == executable_statements
124
+
125
+ def execute_sequence(self, sequence, contracts: List[Contract]):
126
+ """Execute a sequence and check contract violations"""
127
+ func_name, args_dict = sequence
128
+ args = tuple(args_dict.values()) # Convert dict values to tuple
129
+
130
+ try:
131
+ # Use module from analysis context if available
132
+ module = self.analysis_context.module
133
+
134
+ if self._analysis_context.class_name:
135
+ cls = getattr(module, self._analysis_context.class_name, None)
136
+ if cls is None:
137
+ raise AttributeError(f"Class '{self._analysis_context.class_name}' not found")
138
+ obj = cls() # Instantiate the class
139
+ func = getattr(obj, func_name, None)
140
+
141
+ import inspect
142
+ sig = inspect.signature(func)
143
+ param_names = [p.name for p in sig.parameters.values() if p.name != 'self']
144
+ else:
145
+ func = getattr(module, func_name, None)
146
+
147
+ import inspect
148
+ sig = inspect.signature(func)
149
+ param_names = [p.name for p in sig.parameters.values()]
150
+
151
+ # Create ordered arguments based on function signature
152
+ ordered_args = []
153
+ for name in param_names:
154
+ if name in args_dict:
155
+ ordered_args.append(args_dict[name])
156
+
157
+ # Check preconditions
158
+ for contract in contracts:
159
+ if not contract.check_preconditions(tuple(ordered_args)):
160
+ print(f"Preconditions failed for {func_name} with {tuple(ordered_args)}")
161
+ return None, True
162
+
163
+ # Execute function with properly ordered arguments
164
+ output = func(*ordered_args)
165
+ exception = None
166
+
167
+ except Exception as e:
168
+ print(f"EXCEPTION IN RANDOM FEEDBACK: {e}")
169
+ print(traceback.format_exc())
170
+ output = None
171
+ exception = e
172
+
173
+ # Check postconditions
174
+ for contract in contracts:
175
+ if not contract.check_postconditions(tuple(ordered_args), output, exception):
176
+ print(f"Postcondition failed for {func_name} with {tuple(ordered_args)}")
177
+ return output, True
178
+
179
+ return output, False
180
+
181
+
182
+ # TODO: Currently only getting random vals of primitives, extend to sequences
183
+ def random_seqs_and_vals(self, param_types, non_error_seqs=None):
184
+ return self.generate_random_inputs(param_types)
185
+
186
+ @staticmethod
187
+ def extract_parameter_types(func_node):
188
+ """Extract parameter types from a function node."""
189
+ param_types = {}
190
+ for arg in func_node.args.args:
191
+ param_name = arg.arg
192
+ if arg.annotation:
193
+ param_type = ast.unparse(arg.annotation)
194
+ param_types[param_name] = param_type
195
+ else:
196
+ if param_name != 'self':
197
+ param_types[param_name] = None
198
+ return param_types
199
+
200
+ @staticmethod
201
+ def generate_random_inputs(param_types):
202
+ """Generate inputs for fuzzing based on parameter types."""
203
+ inputs = {}
204
+ for param, param_type in param_types.items():
205
+ if param_type == "int":
206
+ random_integer = random.randint(1, 100)
207
+ inputs[param] = random_integer
208
+ if param_type == "bool":
209
+ random_choice = random.choice([True, False])
210
+ inputs[param] = random_choice
211
+ if param_type == "float":
212
+ random_float = random.random()
213
+ inputs[param] = random_float
214
+ # TODO: Random String and Random bytes; Random objects?
215
+ if param_type == "str":
216
+ inputs[param] = "abc"
217
+ #elif param_type == "bytes":
218
+ # inputs[param] = fdp.ConsumeBytes(10)
219
+ #else:
220
+ # inputs[param] = None
221
+ return inputs
222
+
223
+ def collect_test_cases(self, function_metadata: FunctionMetadata) -> List[TestCase]:
224
+ """Collect test cases using random feedback technique"""
225
+ error_seqs, non_error_seqs = self.generate_sequences_new()
226
+ test_cases = []
227
+
228
+ # Process error sequences
229
+ if error_seqs:
230
+ for error_seq in error_seqs:
231
+ print("ERROR SEQ OUTPUT:", error_seq[1])
232
+ test_cases.append(TestCase(error_seq[0][0], tuple(error_seq[0][1].values()), error_seq[1]))
233
+
234
+ # Process non-error sequences
235
+ if non_error_seqs:
236
+ for non_error_seq in non_error_seqs:
237
+ print("NON ERROR SEQ OUTPUT:", non_error_seq[1])
238
+ test_cases.append(TestCase(non_error_seq[0][0], tuple(non_error_seq[0][1].values()), non_error_seq[1]))
239
+
240
+ return self.test_cases
241
+
242
+ def get_all_executable_statements(self, func: FunctionMetadata):
243
+ """Get all executable statements including else branches"""
244
+ import ast
245
+
246
+ test_cases = [tc for tc in self.test_cases if tc.func_name == func.function_name]
247
+
248
+ if not test_cases:
249
+ print("Warning: No test cases available to determine executable statements")
250
+ from testgen.util.randomizer import new_random_test_case
251
+ temp_case = new_random_test_case(self._analysis_context.filepath, func.func_def)
252
+ analysis = coverage_utils.get_coverage_analysis(self._analysis_context.filepath, func.function_name,
253
+ temp_case.inputs)
254
+ else:
255
+ analysis = coverage_utils.get_coverage_analysis(self._analysis_context.filepath, func.function_name, test_cases[0].inputs)
256
+
257
+ # Get standard executable lines from coverage.py
258
+ executable_lines = list(analysis[1])
259
+
260
+ # Parse the source file to find else branches
261
+ with open(self._analysis_context.filepath, 'r') as f:
262
+ source = f.read()
263
+
264
+ # Parse the code
265
+ tree = ast.parse(source)
266
+
267
+ # Find our specific function
268
+ for node in ast.walk(tree):
269
+ if isinstance(node, ast.FunctionDef) and node.name == func.func_def.name:
270
+ # Find all if statements in this function
271
+ for if_node in ast.walk(node):
272
+ if isinstance(if_node, ast.If) and if_node.orelse:
273
+ # There's an else branch
274
+ if isinstance(if_node.orelse[0], ast.If):
275
+ # This is an elif - already counted
276
+ continue
277
+
278
+ # Get the line number of the first statement in the else block
279
+ # and subtract 1 to get the 'else:' line
280
+ else_line = if_node.orelse[0].lineno - 1
281
+
282
+ # Check if this is actually an else line (not a nested if)
283
+ with open(self._analysis_context.filepath, 'r') as f:
284
+ lines = f.readlines()
285
+ if else_line <= len(lines):
286
+ line_content = lines[else_line - 1].strip()
287
+ if line_content == "else:":
288
+ if else_line not in executable_lines:
289
+ executable_lines.append(else_line)
290
+
291
+ return sorted(executable_lines)