Skip to content

Commit 1dfcc51

Browse files
Initial commit of mutation test.
1 parent e9f9467 commit 1dfcc51

File tree

6 files changed

+198
-8
lines changed

6 files changed

+198
-8
lines changed

cover_agent/CoverAgent.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __init__(self, args):
3030
llm_model=args.model,
3131
api_base=args.api_base,
3232
use_report_coverage_feature_flag=args.use_report_coverage_feature_flag,
33+
mutation_testing=args.mutation_testing,
3334
)
3435

3536
def _validate_paths(self):
@@ -89,6 +90,8 @@ def run(self):
8990
if self.test_gen.current_coverage < (self.test_gen.desired_coverage / 100):
9091
self.test_gen.run_coverage()
9192

93+
self.test_gen.run_mutations()
94+
9295
if self.test_gen.current_coverage >= (self.test_gen.desired_coverage / 100):
9396
self.logger.info(
9497
f"Reached above target coverage of {self.test_gen.desired_coverage}% (Current Coverage: {round(self.test_gen.current_coverage * 100, 2)}%) in {iteration_count} iterations."

cover_agent/PromptBuilder.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(
4242
additional_instructions: str = "",
4343
failed_test_runs: str = "",
4444
language: str = "python",
45+
mutation_testing: bool = False,
4546
):
4647
"""
4748
The `PromptBuilder` class is responsible for building a formatted prompt string by replacing placeholders with the actual content of files read during initialization. It takes in various paths and settings as parameters and provides a method to generate the prompt.
@@ -72,6 +73,7 @@ def __init__(
7273
self.test_file = self._read_file(test_file_path)
7374
self.code_coverage_report = code_coverage_report
7475
self.language = language
76+
self.mutation_testing = mutation_testing
7577
# add line numbers to each line in 'source_file'. start from 1
7678
self.source_file_numbered = "\n".join(
7779
[f"{i + 1} {line}" for i, line in enumerate(self.source_file.split("\n"))]
@@ -141,12 +143,20 @@ def build_prompt(self) -> dict:
141143
}
142144
environment = Environment(undefined=StrictUndefined)
143145
try:
144-
system_prompt = environment.from_string(
145-
get_settings().test_generation_prompt.system
146-
).render(variables)
147-
user_prompt = environment.from_string(
148-
get_settings().test_generation_prompt.user
149-
).render(variables)
146+
if self.mutation_testing:
147+
system_prompt = environment.from_string(
148+
get_settings().mutation_test_prompt.system
149+
).render(variables)
150+
user_prompt = environment.from_string(
151+
get_settings().mutation_test_prompt.user
152+
).render(variables)
153+
else:
154+
system_prompt = environment.from_string(
155+
get_settings().test_generation_prompt.system
156+
).render(variables)
157+
user_prompt = environment.from_string(
158+
get_settings().test_generation_prompt.user
159+
).render(variables)
150160
except Exception as e:
151161
logging.error(f"Error rendering prompt: {e}")
152162
return {"system": "", "user": ""}

cover_agent/UnitTestGenerator.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import re
55
import json
6+
67
from wandb.sdk.data_types.trace_tree import Trace
78

89
from cover_agent.Runner import Runner
@@ -14,6 +15,10 @@
1415
from cover_agent.utils import load_yaml
1516
from cover_agent.settings.config_loader import get_settings
1617

18+
import subprocess
19+
20+
from shlex import split
21+
1722

1823
class UnitTestGenerator:
1924
def __init__(
@@ -30,6 +35,7 @@ def __init__(
3035
desired_coverage: int = 90, # Default to 90% coverage if not specified
3136
additional_instructions: str = "",
3237
use_report_coverage_feature_flag: bool = False,
38+
mutation_testing: bool = False,
3339
):
3440
"""
3541
Initialize the UnitTestGenerator class with the provided parameters.
@@ -65,6 +71,7 @@ def __init__(
6571
self.additional_instructions = additional_instructions
6672
self.language = self.get_code_language(source_file_path)
6773
self.use_report_coverage_feature_flag = use_report_coverage_feature_flag
74+
self.mutation_testing = mutation_testing
6875
self.last_coverage_percentages = {}
6976
self.llm_model = llm_model
7077

@@ -79,7 +86,7 @@ def __init__(
7986
self.failed_test_runs = []
8087
self.total_input_token_count = 0
8188
self.total_output_token_count = 0
82-
89+
8390
# Run coverage and build the prompt
8491
self.run_coverage()
8592
self.prompt = self.build_prompt()
@@ -202,7 +209,7 @@ def run_coverage(self):
202209
"Will default to using the full coverage report. You will need to check coverage manually for each passing test."
203210
)
204211
with open(self.code_coverage_report_path, "r") as f:
205-
self.code_coverage_report = f.read()
212+
self.code_coverage_report = f.read()
206213

207214
@staticmethod
208215
def get_included_files(included_files):
@@ -739,6 +746,90 @@ def to_dict(self):
739746
def to_json(self):
740747
return json.dumps(self.to_dict())
741748

749+
def run_mutations(self):
750+
self.logger.info("Running mutation tests")
751+
752+
# Run mutation tests
753+
754+
mutation_prompt_builder = PromptBuilder(
755+
source_file_path=self.source_file_path,
756+
test_file_path=self.test_file_path,
757+
code_coverage_report=self.code_coverage_report,
758+
included_files=self.included_files,
759+
additional_instructions=self.additional_instructions,
760+
failed_test_runs=self.failed_test_runs,
761+
language=self.language,
762+
mutation_testing=True
763+
)
764+
765+
mutation_prompt = mutation_prompt_builder.build_prompt()
766+
767+
response, prompt_token_count, response_token_count = (
768+
self.ai_caller.call_model(prompt=mutation_prompt)
769+
)
770+
771+
mutation_dict = load_yaml(response)
772+
773+
for mutation in mutation_dict["mutation"]:
774+
result = self.run_mutation(mutation)
775+
self.logger.info(f"Mutation result: {result}")
776+
777+
778+
def run_mutation(self, mutation):
779+
mutated_code = mutation.get("mutation", None)
780+
line_number = mutation.get("line", None)
781+
782+
783+
# Read the original content
784+
with open(self.source_file_path, "r") as source_file:
785+
original_content = source_file.readlines()
786+
787+
# Determine the indentation level of the line at line_number
788+
indentation = len(original_content[line_number]) - len(original_content[line_number].lstrip())
789+
790+
# Adjust the indentation of the mutated code
791+
adjusted_mutated_code = [
792+
' ' * indentation + line if line.strip() else line
793+
for line in mutated_code.split("\n")
794+
]
795+
796+
# Insert the mutated code at the specified spot
797+
modified_content = (
798+
original_content[:line_number - 1]
799+
+ adjusted_mutated_code
800+
+ original_content[line_number:]
801+
)
802+
803+
# Write the modified content back to the file
804+
with open(self.source_file_path, "w") as source_file:
805+
source_file.writelines(modified_content)
806+
source_file.flush()
807+
808+
# Step 2: Run the test using the Runner class
809+
self.logger.info(
810+
f'Running test with the following command: "{self.test_command}"'
811+
)
812+
stdout, stderr, exit_code, time_of_test_command = Runner.run_command(
813+
command=self.test_command, cwd=self.test_command_dir
814+
)
815+
816+
try:
817+
result = subprocess.run(
818+
split(self.test_command),
819+
text=True,
820+
capture_output=True,
821+
cwd=self.test_command_dir,
822+
timeout=30,
823+
)
824+
except Exception as e:
825+
logging.error(f"Error running test command: {e}")
826+
result = None
827+
finally:
828+
# Write the modified content back to the file
829+
with open(self.source_file_path, "w") as source_file:
830+
source_file.writelines(original_content)
831+
source_file.flush()
832+
return result
742833

743834
def extract_error_message_python(fail_message):
744835
"""

cover_agent/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ def parse_args():
101101
default="",
102102
help="Path to optional log database. Default: %(default)s.",
103103
)
104+
parser.add_argument(
105+
"--mutation-testing",
106+
action="store_true",
107+
help="Setting this to True enables mutation testing. Default: False.",
108+
)
104109
return parser.parse_args()
105110

106111

cover_agent/settings/config_loader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"language_extensions.toml",
88
"analyze_suite_test_headers_indentation.toml",
99
"analyze_suite_test_insert_line.toml",
10+
"mutation_test_prompt.toml",
1011
]
1112

1213

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
[mutation_test_prompt]
2+
system="""\
3+
"""
4+
5+
user="""\
6+
7+
You are an AI mutation testing agent tasked with mutating {{ language }} code to evaluate its robustness.
8+
9+
Mutation Strategy:
10+
11+
1. Logic Tweaks:
12+
Modify conditions (e.g., 'if (a < b)' to 'if (a <= b)')
13+
Adjust loop boundaries
14+
Introduce minor calculation errors
15+
Avoid drastic changes or infinite loops.
16+
17+
2. Output Modifications:
18+
Change return types or formats
19+
Alter response structures
20+
Return corrupted or incorrect data
21+
22+
3. Method Interference:
23+
Alter function parameters
24+
Replace or omit key method calls
25+
26+
4. Failure Injection:
27+
Introduce exceptions or error states
28+
Simulate system or resource failures
29+
30+
5.Data Handling Faults:
31+
Inject parsing errors
32+
Bypass data validation
33+
Corrupt object states
34+
35+
6. Boundary Condition Testing:
36+
Use out-of-bounds indices
37+
Test extreme or edge-case parameters
38+
39+
7. Concurrency Issues:
40+
Simulate race conditions or deadlocks
41+
Introduce timeouts or delays
42+
43+
8. Security Vulnerabilities:
44+
Replicate common vulnerabilities (e.g., buffer overflow, SQL injection, XSS)
45+
Introduce authentication or authorization bypasses
46+
47+
48+
Focus on subtle, realistic mutations that challenge the code's resilience while keeping core functionality intact. Prioritize scenarios likely to arise from programming errors or edge cases.
49+
50+
51+
## Source Code to add Mutations to: {{ source_file_name }}
52+
```{{language}}
53+
{{ source_file_numbered }}
54+
```
55+
56+
## Task
57+
1. Conduct a line-by-line analysis of the source code.
58+
2. Generate mutations for each test case.
59+
3. Prioritize mutating function blocks and critical code sections.
60+
4. Ensure the mutations offer meaningful insights into code quality and test coverage.
61+
5. Present the output in order of ascending line numbers.
62+
6. Avoid including manually inserted line numbers in the response.
63+
7. Limit mutations to single-line changes only.
64+
65+
Example output:
66+
```yaml
67+
source: {{source_file}}
68+
mutation:
69+
line: <line number>
70+
mutation: |
71+
<mutated code>
72+
```
73+
74+
Use block scalar('|') to format each YAML output.
75+
76+
Response (should be a valid YAML, and nothing else):
77+
```yaml
78+
79+
Generate mutants that test the code’s resilience while preserving core functionality. Output only in YAML format, with no additional explanations or comments.
80+
"""

0 commit comments

Comments
 (0)