Skip to content

Commit d8dd3c4

Browse files
committed
feat: add token cost to mutation coverage
1 parent 48be8a1 commit d8dd3c4

File tree

8 files changed

+48
-23
lines changed

8 files changed

+48
-23
lines changed

β€ŽREADME.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Mutahunter uses LLM models to inject context-aware faults into your codebase. Th
4646

4747
## Getting Started
4848

49+
⚠️ We highly suggest using `--modified-files-only` flag to run mutation testing on only on modified files and `--only-mutate-file-paths` flag to focus on specific files. This will make the mutation testing **faster** and **cost effective.** ⚠️
50+
4951
```bash
5052
# Install Mutahunter package via GitHub. Python 3.11+ is required.
5153
$ pip install git+https://github.com/codeintegrity-ai/mutahunter.git
@@ -73,6 +75,7 @@ $ mutahunter run --test-command "pytest tests/unit" --code-coverage-report-path
7375
2024-07-05 00:26:13,421 INFO: πŸ•’ Timeout Mutants: 0 πŸ•’
7476
2024-07-05 00:26:13,421 INFO: πŸ”₯ Compile Error Mutants: 0 πŸ”₯
7577
2024-07-05 00:26:13,421 INFO: 🎯 Mutation Coverage: 61.54% 🎯
78+
2024-07-05 00:26:13,421 INFO: πŸ’° Total Cost: $0.00583 USD πŸ’°
7679
2024-07-05 00:26:13,421 INFO: Report saved to logs/_latest/mutation_coverage.json
7780
2024-07-05 00:26:13,421 INFO: Report saved to logs/_latest/mutation_coverage_detail.json
7881
2024-07-05 00:26:13,421 INFO: Mutation Testing Ended. Took 43s

β€Žsrc/mutahunter/core/hunter.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from mutahunter.core.logger import logger
1111
from mutahunter.core.mutator import MutantGenerator
1212
from mutahunter.core.report import MutantReport
13+
from mutahunter.core.router import LLMRouter
1314
from mutahunter.core.runner import TestRunner
1415

1516

@@ -23,6 +24,9 @@ def __init__(self, config: Dict[str, Any]) -> None:
2324
self.mutant_report = MutantReport(config=self.config)
2425
self.analyzer = Analyzer(self.config)
2526
self.test_runner = TestRunner()
27+
self.router = LLMRouter(
28+
model=self.config["model"], api_base=self.config["api_base"]
29+
)
2630

2731
def run(self) -> None:
2832
"""
@@ -40,7 +44,7 @@ def run(self) -> None:
4044
logger.info("🦠 Running mutation testing on entire codebase... 🦠")
4145
self.run_mutation_testing()
4246
logger.info("🎯 Generating Mutation Report... 🎯")
43-
self.mutant_report.generate_report(self.mutants)
47+
self.mutant_report.generate_report(self.mutants, self.router.total_cost)
4448
logger.info(f"Mutation Testing Ended. Took {round(time.time() - start)}s")
4549
except Exception as e:
4650
logger.error(
@@ -151,6 +155,7 @@ def generate_mutations(self, file_path: str, executed_lines: List[int]) -> None:
151155
function_name=function_name,
152156
start_byte=start_byte,
153157
end_byte=end_byte,
158+
router=self.router,
154159
)
155160
for mutant_data in mutant_generator.generate():
156161
self.process_mutant(mutant_data, file_path, start_byte, end_byte)

β€Žsrc/mutahunter/core/mutator.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from mutahunter.core.logger import logger
77
from mutahunter.core.pilot.aider.repomap import RepoMap
88
from mutahunter.core.pilot.prompts.factory import PromptFactory
9-
from mutahunter.core.router import LLMRouter
109

1110

1211
class MutantGenerator:
@@ -19,6 +18,7 @@ def __init__(
1918
function_name,
2019
start_byte,
2120
end_byte,
21+
router,
2222
):
2323
self.config = config
2424
self.executed_lines = executed_lines
@@ -27,10 +27,7 @@ def __init__(
2727
self.function_name = function_name
2828
self.start_byte = start_byte
2929
self.end_byte = end_byte
30-
31-
self.router = LLMRouter(
32-
model=self.config["model"], api_base=self.config["api_base"]
33-
)
30+
self.router = router
3431
self.repo_map = RepoMap(model=self.config["model"])
3532
self.language = filename_to_lang(self.source_file_path)
3633
self.prompt = PromptFactory.get_prompt(language=self.language)

β€Žsrc/mutahunter/core/pilot/prompts/factory.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
Module for generating prompts based on the programming language.
33
"""
44

5-
from mutahunter.core.pilot.prompts.examples import (
6-
GO_EXAMPLE_OUTPUT,
7-
JAVA_EXAMPLE_OUTPUT,
8-
JAVASCRIPT_EXAMPLE_OUTPUT,
9-
PYTHON_EXAMPLE_OUTPUT,
10-
)
5+
from mutahunter.core.pilot.prompts.examples import (GO_EXAMPLE_OUTPUT,
6+
JAVA_EXAMPLE_OUTPUT,
7+
JAVASCRIPT_EXAMPLE_OUTPUT,
8+
PYTHON_EXAMPLE_OUTPUT)
119
from mutahunter.core.pilot.prompts.system import SYSTEM_PROMPT
1210
from mutahunter.core.pilot.prompts.user import USER_PROMPT
1311

β€Žsrc/mutahunter/core/report.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class MutantReport:
2222
def __init__(self, config) -> None:
2323
self.config = config
2424

25-
def generate_report(self, mutants: List[Mutant]) -> None:
25+
def generate_report(self, mutants: List[Mutant], total_cost) -> None:
2626
"""
2727
Generates a comprehensive mutation testing report.
2828
@@ -32,10 +32,10 @@ def generate_report(self, mutants: List[Mutant]) -> None:
3232
mutants = [asdict(mutant) for mutant in mutants]
3333
self.save_report("logs/_latest/mutants.json", mutants)
3434
print(MUTAHUNTER_ASCII)
35-
self.generate_mutant_report(mutants)
35+
self.generate_mutant_report(mutants, total_cost)
3636
self.generate_mutant_report_detail(mutants)
3737

38-
def generate_mutant_report(self, mutants: List[Mutant]) -> None:
38+
def generate_mutant_report(self, mutants: List[Mutant], total_cost) -> None:
3939
killed_mutants = [mutant for mutant in mutants if mutant["status"] == "KILLED"]
4040
survived_mutants = [
4141
mutant for mutant in mutants if mutant["status"] == "SURVIVED"
@@ -62,6 +62,7 @@ def generate_mutant_report(self, mutants: List[Mutant]) -> None:
6262
logger.info("πŸ•’ Timeout Mutants: %d πŸ•’", len(timeout_mutants))
6363
logger.info("πŸ”₯ Compile Error Mutants: %d πŸ”₯", len(compile_error_mutants))
6464
logger.info("🎯 Mutation Coverage: %s 🎯", total_mutation_coverage)
65+
logger.info("πŸ’° Expected Cost: $%.5f USD πŸ’°", total_cost)
6566

6667
mutation_coverage = {
6768
"total_mutants": len(mutants),
@@ -70,6 +71,7 @@ def generate_mutant_report(self, mutants: List[Mutant]) -> None:
7071
"timeout_mutants": len(timeout_mutants),
7172
"compile_error_mutants": len(compile_error_mutants),
7273
"mutation_coverage": total_mutation_coverage,
74+
"expected_cost": total_cost,
7375
}
7476

7577
self.save_report("logs/_latest/mutation_coverage.json", mutation_coverage)

β€Žsrc/mutahunter/core/router.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import time
22

3-
import litellm
3+
from litellm import completion, completion_cost, litellm, success_callback
44

55

66
class LLMRouter:
@@ -10,6 +10,24 @@ def __init__(self, model: str, api_base: str = ""):
1010
"""
1111
self.model = model
1212
self.api_base = api_base
13+
self.total_cost = 0
14+
litellm.success_callback = [self.track_cost_callback]
15+
16+
# track_cost_callback
17+
18+
def track_cost_callback(
19+
self,
20+
kwargs, # kwargs to completion
21+
completion_response, # response from completion
22+
start_time,
23+
end_time, # start/end time
24+
):
25+
try:
26+
response_cost = kwargs.get("response_cost", 0)
27+
self.total_cost += response_cost
28+
print("streaming response_cost", response_cost)
29+
except:
30+
pass
1331

1432
def generate_response(
1533
self, prompt: dict, max_tokens: int = 4096, streaming: bool = False
@@ -25,6 +43,7 @@ def generate_response(
2543
Returns:
2644
tuple: Generated response, prompt tokens used, and completion tokens used.
2745
"""
46+
streaming = False
2847
self._validate_prompt(prompt)
2948
messages = self._build_messages(prompt)
3049
completion_params = self._build_completion_params(
@@ -86,7 +105,7 @@ def _stream_response(self, completion_params: dict) -> list:
86105
"""
87106
response_chunks = []
88107
print("\nStreaming results from LLM model...")
89-
response = litellm.completion(**completion_params)
108+
response = completion(**completion_params)
90109
for chunk in response:
91110
print(chunk.choices[0].delta.content or "", end="", flush=True)
92111
response_chunks.append(chunk)
@@ -100,7 +119,7 @@ def _non_stream_response(self, completion_params: dict) -> tuple:
100119
"""
101120
Get the non-streamed response from the LLM model.
102121
"""
103-
response = litellm.completion(**completion_params)
122+
response = completion(**completion_params)
104123
content = response["choices"][0]["message"]["content"]
105124
prompt_tokens = int(response["usage"]["prompt_tokens"])
106125
completion_tokens = int(response["usage"]["completion_tokens"])

β€Žtests/test_hunter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,6 @@ def test_should_skip_file_exclude_files(mutant_hunter):
326326
assert mutant_hunter.should_skip_file("excluded_file.py") is True
327327

328328

329-
330329
def test_process_test_result_compile_error(mutant_hunter):
331330
mutant = Mutant(
332331
id="1",
@@ -356,6 +355,7 @@ def test_process_test_result_timeout(mutant_hunter):
356355
assert mutant.status == "TIMEOUT"
357356
assert mutant.error_msg == "timeout error"
358357

358+
359359
@patch.object(MutantHunter, "prepare_mutant_file", return_value="")
360360
def test_process_mutant_compile_error(mock_prepare_mutant_file, mutant_hunter):
361361
mutant_data = {"mutant_code": "code", "type": "type", "description": "description"}

β€Žtests/test_report.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import json
2+
from dataclasses import asdict
23
from unittest.mock import mock_open, patch
34

45
import pytest
5-
from dataclasses import asdict
6+
67
from mutahunter.core.entities.mutant import Mutant
78
from mutahunter.core.report import MutantReport
89

9-
from unittest.mock import patch
10-
1110

1211
@pytest.fixture
1312
def config():
@@ -135,14 +134,15 @@ def test_generate_mutant_report(mutants, config):
135134
patch.object(report, "save_report") as mock_save_report,
136135
patch("mutahunter.core.logger.logger.info") as mock_logger_info,
137136
):
138-
report.generate_mutant_report(mutants)
137+
report.generate_mutant_report(mutants, 0.0)
139138

140139
mock_logger_info.assert_any_call("🦠 Total Mutants: %d 🦠", len(mutants))
141140
mock_logger_info.assert_any_call("πŸ›‘οΈ Survived Mutants: %d πŸ›‘οΈ", 2)
142141
mock_logger_info.assert_any_call("πŸ—‘οΈ Killed Mutants: %d πŸ—‘οΈ", 2)
143142
mock_logger_info.assert_any_call("πŸ•’ Timeout Mutants: %d πŸ•’", 0)
144143
mock_logger_info.assert_any_call("πŸ”₯ Compile Error Mutants: %d πŸ”₯", 0)
145144
mock_logger_info.assert_any_call("🎯 Mutation Coverage: %s 🎯", "50.00%")
145+
mock_logger_info.assert_any_call("πŸ’° Expected Cost: $%.5f USD πŸ’°", 0.0)
146146

147147
mock_save_report.assert_called_once_with(
148148
"logs/_latest/mutation_coverage.json",
@@ -153,6 +153,7 @@ def test_generate_mutant_report(mutants, config):
153153
"timeout_mutants": 0,
154154
"compile_error_mutants": 0,
155155
"mutation_coverage": "50.00%",
156+
"expected_cost": 0.0,
156157
},
157158
)
158159

0 commit comments

Comments
Β (0)