dataframe-mutator 0.1.1__tar.gz → 0.2.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.
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/PKG-INFO +2 -1
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/dataframe_mutator.egg-info/PKG-INFO +2 -1
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/dataframe_mutator.egg-info/SOURCES.txt +7 -0
- dataframe_mutator-0.2.0/dataframe_mutator.egg-info/entry_points.txt +5 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/dataframe_mutator.egg-info/requires.txt +1 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/pyproject.toml +8 -1
- dataframe_mutator-0.2.0/src/dataframe_mutator/cli.py +159 -0
- dataframe_mutator-0.2.0/src/dataframe_mutator/config.py +95 -0
- dataframe_mutator-0.2.0/src/dataframe_mutator/integrations.py +170 -0
- dataframe_mutator-0.2.0/src/dataframe_mutator/pytest_plugin.py +78 -0
- dataframe_mutator-0.2.0/src/dataframe_mutator/reports.py +181 -0
- dataframe_mutator-0.2.0/tests/test_advanced_features.py +177 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/LICENSE +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/README.md +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/dataframe_mutator.egg-info/dependency_links.txt +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/dataframe_mutator.egg-info/top_level.txt +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/setup.cfg +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/src/dataframe_mutator/__init__.py +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/tests/test_polars_100_coverage.py +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/tests/test_polars_new_operators.py +0 -0
- {dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/tests/test_polars_operators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-mutator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Production-grade mutation testing for Polars dataframes. Validate test suite quality by detecting which mutations your tests catch.
|
|
5
5
|
Author-email: Dataframe Mutator Contributors <suhrusai@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,7 @@ Requires-Python: >=3.8
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: mutmut>=2.4.0
|
|
29
|
+
Requires-Dist: click>=8.0.0
|
|
29
30
|
Provides-Extra: polars
|
|
30
31
|
Requires-Dist: polars>=0.19.0; extra == "polars"
|
|
31
32
|
Provides-Extra: pyspark
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-mutator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Production-grade mutation testing for Polars dataframes. Validate test suite quality by detecting which mutations your tests catch.
|
|
5
5
|
Author-email: Dataframe Mutator Contributors <suhrusai@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,6 +26,7 @@ Requires-Python: >=3.8
|
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Dist: mutmut>=2.4.0
|
|
29
|
+
Requires-Dist: click>=8.0.0
|
|
29
30
|
Provides-Extra: polars
|
|
30
31
|
Requires-Dist: polars>=0.19.0; extra == "polars"
|
|
31
32
|
Provides-Extra: pyspark
|
|
@@ -4,9 +4,16 @@ pyproject.toml
|
|
|
4
4
|
dataframe_mutator.egg-info/PKG-INFO
|
|
5
5
|
dataframe_mutator.egg-info/SOURCES.txt
|
|
6
6
|
dataframe_mutator.egg-info/dependency_links.txt
|
|
7
|
+
dataframe_mutator.egg-info/entry_points.txt
|
|
7
8
|
dataframe_mutator.egg-info/requires.txt
|
|
8
9
|
dataframe_mutator.egg-info/top_level.txt
|
|
9
10
|
src/dataframe_mutator/__init__.py
|
|
11
|
+
src/dataframe_mutator/cli.py
|
|
12
|
+
src/dataframe_mutator/config.py
|
|
13
|
+
src/dataframe_mutator/integrations.py
|
|
14
|
+
src/dataframe_mutator/pytest_plugin.py
|
|
15
|
+
src/dataframe_mutator/reports.py
|
|
16
|
+
tests/test_advanced_features.py
|
|
10
17
|
tests/test_polars_100_coverage.py
|
|
11
18
|
tests/test_polars_new_operators.py
|
|
12
19
|
tests/test_polars_operators.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dataframe-mutator"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Production-grade mutation testing for Polars dataframes. Validate test suite quality by detecting which mutations your tests catch."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -30,8 +30,15 @@ classifiers = [
|
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
32
|
"mutmut>=2.4.0",
|
|
33
|
+
"click>=8.0.0",
|
|
33
34
|
]
|
|
34
35
|
|
|
36
|
+
[project.scripts]
|
|
37
|
+
dataframe-mutator = "dataframe_mutator.cli:cli"
|
|
38
|
+
|
|
39
|
+
[project.entry-points.pytest11]
|
|
40
|
+
dataframe-mutator = "dataframe_mutator.pytest_plugin"
|
|
41
|
+
|
|
35
42
|
[project.urls]
|
|
36
43
|
Homepage = "https://github.com/suhrusai/dataframe-mutator"
|
|
37
44
|
Documentation = "https://github.com/suhrusai/dataframe-mutator#readme"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Command-line interface for dataframe-mutator."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from .config import MutationConfig
|
|
7
|
+
from .polars import SmartPolarsTestRunner
|
|
8
|
+
from .reports import HTMLReportGenerator, JSONExporter, BaselineTracker
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def cli():
|
|
13
|
+
"""dataframe-mutator - Mutation testing for Polars dataframes."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@cli.command()
|
|
18
|
+
@click.argument("source", type=click.Path(exists=True), nargs=-1, required=True)
|
|
19
|
+
@click.option("--tests", default="tests/", help="Path to test directory")
|
|
20
|
+
@click.option("--config", default="dataframe-mutator.toml", help="Config file path")
|
|
21
|
+
@click.option("--output", default="text", type=click.Choice(["text", "json", "html"]),
|
|
22
|
+
help="Output format")
|
|
23
|
+
@click.option("--threshold", type=float, help="Mutation score threshold")
|
|
24
|
+
@click.option("--parallel", is_flag=True, default=True, help="Use parallel testing")
|
|
25
|
+
@click.option("--workers", type=int, help="Number of parallel workers")
|
|
26
|
+
@click.option("--save-baseline", is_flag=True, help="Save current results as baseline")
|
|
27
|
+
def analyze(source, tests, config, output, threshold, parallel, workers, save_baseline):
|
|
28
|
+
"""Analyze mutation efficiency of your code."""
|
|
29
|
+
|
|
30
|
+
# Load configuration
|
|
31
|
+
cfg = MutationConfig.from_toml(config)
|
|
32
|
+
|
|
33
|
+
# Override with CLI arguments
|
|
34
|
+
if threshold:
|
|
35
|
+
cfg.mutation_threshold = threshold
|
|
36
|
+
if not parallel:
|
|
37
|
+
cfg.parallel = False
|
|
38
|
+
if workers:
|
|
39
|
+
cfg.num_workers = workers
|
|
40
|
+
cfg.output_format = output
|
|
41
|
+
cfg.source_files = list(source)
|
|
42
|
+
cfg.test_command = f"pytest {tests}"
|
|
43
|
+
|
|
44
|
+
click.echo(f"[*] Analyzing {len(source)} file(s)...")
|
|
45
|
+
click.echo(f"[TEST] Test command: {cfg.test_command}")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
tester = SmartPolarsTestRunner(
|
|
49
|
+
test_command=cfg.test_command,
|
|
50
|
+
parallel=cfg.parallel,
|
|
51
|
+
num_workers=cfg.num_workers
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
all_results = {}
|
|
55
|
+
for src_file in source:
|
|
56
|
+
click.echo(f"\n[FILE] {src_file}")
|
|
57
|
+
results = tester.analyze_mutation_efficiency(src_file)
|
|
58
|
+
all_results[src_file] = results
|
|
59
|
+
|
|
60
|
+
click.echo(f" [OK] High-value mutations: {results['high_value_mutations']}")
|
|
61
|
+
click.echo(f" [STAT] False positives avoided: {results['potential_false_positives_avoided']:.1f}%")
|
|
62
|
+
|
|
63
|
+
if results['high_value_mutations'] == 0:
|
|
64
|
+
click.echo(" [WARN] No mutations found!")
|
|
65
|
+
|
|
66
|
+
# Generate reports
|
|
67
|
+
Path(cfg.output_dir).mkdir(exist_ok=True)
|
|
68
|
+
|
|
69
|
+
if output in ["text", "json", "html"]:
|
|
70
|
+
if output == "json" or output == "html":
|
|
71
|
+
exporter = JSONExporter(cfg)
|
|
72
|
+
exporter.export(all_results, f"{cfg.output_dir}/results.json")
|
|
73
|
+
click.echo(f"\n[REPORT] JSON report: {cfg.output_dir}/results.json")
|
|
74
|
+
|
|
75
|
+
if output == "html":
|
|
76
|
+
generator = HTMLReportGenerator(cfg)
|
|
77
|
+
generator.generate(all_results, f"{cfg.output_dir}/report.html")
|
|
78
|
+
click.echo(f"[REPORT] HTML report: {cfg.output_dir}/report.html")
|
|
79
|
+
|
|
80
|
+
# Save baseline if requested
|
|
81
|
+
if save_baseline:
|
|
82
|
+
tracker = BaselineTracker(cfg)
|
|
83
|
+
tracker.save_baseline(all_results)
|
|
84
|
+
click.echo(f"\n[SAVE] Baseline saved: {cfg.baseline_path}")
|
|
85
|
+
|
|
86
|
+
# Check threshold
|
|
87
|
+
avg_mutations = sum(r['high_value_mutations'] for r in all_results.values()) / len(all_results)
|
|
88
|
+
if avg_mutations < cfg.min_mutations:
|
|
89
|
+
click.echo(f"\n[FAIL] Below minimum mutations ({avg_mutations:.0f} < {cfg.min_mutations})")
|
|
90
|
+
raise click.Exit(1)
|
|
91
|
+
|
|
92
|
+
click.echo("\n[SUCCESS] Analysis complete!")
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
click.echo(f"[ERROR] Error: {e}", err=True)
|
|
96
|
+
raise click.Exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@cli.command()
|
|
100
|
+
@click.argument("source", type=click.Path(exists=True), nargs=-1, required=True)
|
|
101
|
+
@click.option("--tests", default="tests/", help="Path to test directory")
|
|
102
|
+
@click.option("--config", default="dataframe-mutator.toml", help="Config file path")
|
|
103
|
+
def check(source, tests, config):
|
|
104
|
+
"""Check mutation score against baseline."""
|
|
105
|
+
|
|
106
|
+
cfg = MutationConfig.from_toml(config)
|
|
107
|
+
cfg.source_files = list(source)
|
|
108
|
+
cfg.test_command = f"pytest {tests}"
|
|
109
|
+
|
|
110
|
+
tracker = BaselineTracker(cfg)
|
|
111
|
+
|
|
112
|
+
if not Path(cfg.baseline_path).exists():
|
|
113
|
+
click.echo("[FAIL] No baseline found. Run with --save-baseline first.")
|
|
114
|
+
raise click.Exit(1)
|
|
115
|
+
|
|
116
|
+
tester = SmartPolarsTestRunner(test_command=cfg.test_command)
|
|
117
|
+
baseline = tracker.load_baseline()
|
|
118
|
+
|
|
119
|
+
click.echo("[*] Comparing to baseline...")
|
|
120
|
+
|
|
121
|
+
all_pass = True
|
|
122
|
+
for src_file in source:
|
|
123
|
+
results = tester.analyze_mutation_efficiency(src_file)
|
|
124
|
+
current = results['high_value_mutations']
|
|
125
|
+
baseline_val = baseline.get(src_file, {}).get('high_value_mutations', 0)
|
|
126
|
+
|
|
127
|
+
change = current - baseline_val
|
|
128
|
+
if change >= 0:
|
|
129
|
+
click.echo(f"[OK] {src_file}: {current} mutations (+{change})")
|
|
130
|
+
else:
|
|
131
|
+
click.echo(f"[WARN] {src_file}: {current} mutations ({change})")
|
|
132
|
+
all_pass = False
|
|
133
|
+
|
|
134
|
+
if not all_pass:
|
|
135
|
+
click.echo("\n[WARN] Some files have fewer mutations than baseline")
|
|
136
|
+
raise click.Exit(1)
|
|
137
|
+
|
|
138
|
+
click.echo("\n[OK] All files meet or exceed baseline!")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@cli.command()
|
|
142
|
+
@click.option("--config", default="dataframe-mutator.toml", help="Config file path")
|
|
143
|
+
def init(config):
|
|
144
|
+
"""Initialize configuration file."""
|
|
145
|
+
|
|
146
|
+
cfg = MutationConfig()
|
|
147
|
+
cfg.to_json(config)
|
|
148
|
+
click.echo(f"[OK] Created {config}")
|
|
149
|
+
click.echo("Edit this file to customize mutation testing settings.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@cli.command()
|
|
153
|
+
def version():
|
|
154
|
+
"""Show version."""
|
|
155
|
+
click.echo("dataframe-mutator 0.1.1")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
cli()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Configuration management for dataframe-mutator."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass, field, asdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
# Try to import tomllib (Python 3.11+)
|
|
10
|
+
try:
|
|
11
|
+
import tomllib
|
|
12
|
+
except ImportError:
|
|
13
|
+
tomllib = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MutationConfig:
|
|
18
|
+
"""Configuration for mutation testing."""
|
|
19
|
+
|
|
20
|
+
# Paths
|
|
21
|
+
source_files: List[str] = field(default_factory=list)
|
|
22
|
+
test_command: str = "pytest tests/"
|
|
23
|
+
skip_patterns: List[str] = field(default_factory=lambda: ["__pycache__", "*.pyc", ".git"])
|
|
24
|
+
|
|
25
|
+
# Thresholds
|
|
26
|
+
mutation_threshold: float = 0.85 # Fail if below 85%
|
|
27
|
+
min_mutations: int = 5 # Require at least 5 mutations
|
|
28
|
+
|
|
29
|
+
# Operators
|
|
30
|
+
operators: Optional[List[str]] = None # None = all operators
|
|
31
|
+
skip_operators: List[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
# Features
|
|
34
|
+
skip_low_value_mutations: bool = True
|
|
35
|
+
parallel: bool = True
|
|
36
|
+
num_workers: Optional[int] = None # None = auto (cpu_count)
|
|
37
|
+
|
|
38
|
+
# Output
|
|
39
|
+
output_format: str = "text" # text, json, html
|
|
40
|
+
output_dir: str = "mutation-results"
|
|
41
|
+
save_baseline: bool = False
|
|
42
|
+
baseline_path: str = ".mutation-baseline.json"
|
|
43
|
+
|
|
44
|
+
# Integrations
|
|
45
|
+
github_annotations: bool = False
|
|
46
|
+
slack_webhook: Optional[str] = None
|
|
47
|
+
slack_on_fail_only: bool = True
|
|
48
|
+
|
|
49
|
+
# Advanced
|
|
50
|
+
timeout_per_test: Optional[int] = None # Seconds
|
|
51
|
+
max_mutations_per_file: Optional[int] = None
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_toml(cls, path: str = "dataframe-mutator.toml") -> "MutationConfig":
|
|
55
|
+
"""Load configuration from TOML file."""
|
|
56
|
+
config_path = Path(path)
|
|
57
|
+
if not config_path.exists():
|
|
58
|
+
return cls()
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# Try Python 3.11+ tomllib first
|
|
62
|
+
if tomllib:
|
|
63
|
+
with open(config_path, "rb") as f:
|
|
64
|
+
data = tomllib.load(f)
|
|
65
|
+
else:
|
|
66
|
+
# Fallback for older Python
|
|
67
|
+
import tomli
|
|
68
|
+
with open(config_path, "rb") as f:
|
|
69
|
+
data = tomli.load(f)
|
|
70
|
+
|
|
71
|
+
config_data = data.get("dataframe-mutator", {})
|
|
72
|
+
return cls(**config_data)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"Warning: Could not load config from {path}: {e}")
|
|
75
|
+
return cls()
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_json(cls, path: str) -> "MutationConfig":
|
|
79
|
+
"""Load configuration from JSON file."""
|
|
80
|
+
try:
|
|
81
|
+
with open(path) as f:
|
|
82
|
+
data = json.load(f)
|
|
83
|
+
return cls(**data)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Warning: Could not load config from {path}: {e}")
|
|
86
|
+
return cls()
|
|
87
|
+
|
|
88
|
+
def to_json(self, path: str) -> None:
|
|
89
|
+
"""Save configuration to JSON file."""
|
|
90
|
+
with open(path, "w") as f:
|
|
91
|
+
json.dump(asdict(self), f, indent=2)
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict:
|
|
94
|
+
"""Convert to dictionary."""
|
|
95
|
+
return asdict(self)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Integrations with external services and tools."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
from urllib.request import urlopen, Request
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SlackNotifier:
|
|
11
|
+
"""Send mutation testing results to Slack."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, webhook_url: str):
|
|
14
|
+
self.webhook_url = webhook_url
|
|
15
|
+
|
|
16
|
+
def notify(self, results: Dict[str, Any], threshold: float = 0.85) -> None:
|
|
17
|
+
"""Send Slack notification."""
|
|
18
|
+
total_mutations = sum(r.get("high_value_mutations", 0) for r in results.values())
|
|
19
|
+
avg_false_pos = sum(r.get("potential_false_positives_avoided", 0) for r in results.values()) / len(results) if results else 0
|
|
20
|
+
|
|
21
|
+
color = "good" if total_mutations > 0 else "danger"
|
|
22
|
+
|
|
23
|
+
payload = {
|
|
24
|
+
"attachments": [{
|
|
25
|
+
"color": color,
|
|
26
|
+
"title": "🧬 Mutation Testing Results",
|
|
27
|
+
"fields": [
|
|
28
|
+
{"title": "Total Mutations", "value": str(total_mutations), "short": True},
|
|
29
|
+
{"title": "Files Analyzed", "value": str(len(results)), "short": True},
|
|
30
|
+
{"title": "False Positives Avoided", "value": f"{avg_false_pos:.1f}%", "short": True},
|
|
31
|
+
{"title": "Status", "value": "✅ Pass" if total_mutations > 0 else "⚠️ No mutations", "short": True},
|
|
32
|
+
]
|
|
33
|
+
}]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
req = Request(
|
|
38
|
+
self.webhook_url,
|
|
39
|
+
data=json.dumps(payload).encode(),
|
|
40
|
+
headers={"Content-Type": "application/json"}
|
|
41
|
+
)
|
|
42
|
+
urlopen(req)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Warning: Failed to send Slack notification: {e}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GitHubAnnotator:
|
|
48
|
+
"""Add annotations to GitHub Actions workflow."""
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def annotate_mutations(results: Dict[str, Any]) -> None:
|
|
52
|
+
"""Add GitHub workflow annotations for mutations."""
|
|
53
|
+
if not os.getenv("GITHUB_ACTIONS"):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
for file, data in results.items():
|
|
57
|
+
mutations = data.get("high_value_mutations", 0)
|
|
58
|
+
if mutations == 0:
|
|
59
|
+
level = "warning"
|
|
60
|
+
msg = "No mutations detected - may need more tests"
|
|
61
|
+
else:
|
|
62
|
+
level = "notice"
|
|
63
|
+
msg = f"{mutations} high-value mutations found"
|
|
64
|
+
|
|
65
|
+
print(f"::{level} file={file}::{msg}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CustomOperatorRegistry:
|
|
69
|
+
"""Registry for custom mutation operators."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, registry_path: str = ".mutation-operators"):
|
|
72
|
+
self.registry_path = Path(registry_path)
|
|
73
|
+
self.registry_path.mkdir(exist_ok=True)
|
|
74
|
+
|
|
75
|
+
def register(self, operator_class) -> None:
|
|
76
|
+
"""Register a custom operator."""
|
|
77
|
+
operator_file = self.registry_path / f"{operator_class.name}.py"
|
|
78
|
+
|
|
79
|
+
# Save operator code
|
|
80
|
+
import inspect
|
|
81
|
+
code = inspect.getsource(operator_class)
|
|
82
|
+
operator_file.write_text(code)
|
|
83
|
+
|
|
84
|
+
def load_operators(self) -> list:
|
|
85
|
+
"""Load all registered operators."""
|
|
86
|
+
operators = []
|
|
87
|
+
|
|
88
|
+
for op_file in self.registry_path.glob("*.py"):
|
|
89
|
+
if op_file.name == "__init__.py":
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Dynamically import operator
|
|
93
|
+
import importlib.util
|
|
94
|
+
spec = importlib.util.spec_from_file_location("operator", op_file)
|
|
95
|
+
module = importlib.util.module_from_spec(spec)
|
|
96
|
+
spec.loader.exec_module(module)
|
|
97
|
+
|
|
98
|
+
# Find operator class
|
|
99
|
+
for attr in dir(module):
|
|
100
|
+
obj = getattr(module, attr)
|
|
101
|
+
if hasattr(obj, "name") and hasattr(obj, "mutate"):
|
|
102
|
+
operators.append(obj)
|
|
103
|
+
|
|
104
|
+
return operators
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class OperatorSuggester:
|
|
108
|
+
"""Suggest missing mutation operators based on code."""
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def analyze_code(source_file: str) -> Dict[str, int]:
|
|
112
|
+
"""Analyze code and suggest operators."""
|
|
113
|
+
content = Path(source_file).read_text()
|
|
114
|
+
suggestions = {}
|
|
115
|
+
|
|
116
|
+
# Check for patterns
|
|
117
|
+
if "filter(" in content:
|
|
118
|
+
suggestions["filter_mutations"] = content.count("filter(")
|
|
119
|
+
|
|
120
|
+
if ".sum()" in content or ".mean()" in content:
|
|
121
|
+
suggestions["aggregation_mutations"] = (
|
|
122
|
+
content.count(".sum()") + content.count(".mean()")
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if ".join(" in content:
|
|
126
|
+
suggestions["join_mutations"] = content.count(".join(")
|
|
127
|
+
|
|
128
|
+
if "is_null" in content or "fill_null" in content:
|
|
129
|
+
suggestions["null_handling_mutations"] = (
|
|
130
|
+
content.count("is_null") + content.count("fill_null")
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if ">" in content or ">=" in content or "<" in content:
|
|
134
|
+
# Count comparison operators (rough estimate)
|
|
135
|
+
suggestions["boundary_mutations"] = (
|
|
136
|
+
content.count(" > ") + content.count(" >= ") +
|
|
137
|
+
content.count(" < ") + content.count(" <= ")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return suggestions
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ParallelMutationRunner:
|
|
144
|
+
"""Run mutations in parallel for faster testing."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, num_workers: Optional[int] = None):
|
|
147
|
+
import multiprocessing
|
|
148
|
+
self.num_workers = num_workers or multiprocessing.cpu_count()
|
|
149
|
+
|
|
150
|
+
def run_parallel(self, mutations: list, test_command: str) -> Dict[str, bool]:
|
|
151
|
+
"""Run mutations in parallel."""
|
|
152
|
+
from multiprocessing import Pool
|
|
153
|
+
import subprocess
|
|
154
|
+
|
|
155
|
+
def run_mutation(mutation_code: str) -> bool:
|
|
156
|
+
try:
|
|
157
|
+
result = subprocess.run(
|
|
158
|
+
["python", "-c", test_command],
|
|
159
|
+
capture_output=True,
|
|
160
|
+
timeout=30,
|
|
161
|
+
text=True
|
|
162
|
+
)
|
|
163
|
+
return result.returncode == 0
|
|
164
|
+
except:
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
with Pool(self.num_workers) as pool:
|
|
168
|
+
results = pool.map(run_mutation, mutations)
|
|
169
|
+
|
|
170
|
+
return {f"mutation_{i}": passed for i, passed in enumerate(results)}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""pytest plugin for mutation testing integration."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .polars import SmartPolarsTestRunner
|
|
6
|
+
from .config import MutationConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def pytest_addoption(parser):
|
|
10
|
+
"""Add pytest options for mutation testing."""
|
|
11
|
+
parser.addoption(
|
|
12
|
+
"--mutation",
|
|
13
|
+
action="store_true",
|
|
14
|
+
default=False,
|
|
15
|
+
help="Run mutation testing on Polars code"
|
|
16
|
+
)
|
|
17
|
+
parser.addoption(
|
|
18
|
+
"--mutation-source",
|
|
19
|
+
default="src/",
|
|
20
|
+
help="Source directory to analyze"
|
|
21
|
+
)
|
|
22
|
+
parser.addoption(
|
|
23
|
+
"--mutation-threshold",
|
|
24
|
+
type=float,
|
|
25
|
+
default=0.85,
|
|
26
|
+
help="Minimum mutation score threshold"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def pytest_configure(config):
|
|
31
|
+
"""Configure pytest for mutation testing."""
|
|
32
|
+
if config.getoption("--mutation"):
|
|
33
|
+
config.addinivalue_line("markers", "mutation: mark test as mutation testing")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def pytest_collection_modifyitems(config, items):
|
|
37
|
+
"""Modify test collection for mutation testing."""
|
|
38
|
+
if config.getoption("--mutation"):
|
|
39
|
+
# Add mutation marker to all tests
|
|
40
|
+
for item in items:
|
|
41
|
+
item.add_marker(pytest.mark.mutation)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(scope="session")
|
|
45
|
+
def mutation_tester(request):
|
|
46
|
+
"""Provide mutation tester fixture."""
|
|
47
|
+
cfg = MutationConfig.from_toml()
|
|
48
|
+
return SmartPolarsTestRunner(test_command="pytest")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
|
52
|
+
"""Add mutation testing summary to pytest output."""
|
|
53
|
+
if not config.getoption("--mutation"):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
source_dir = config.getoption("--mutation-source")
|
|
57
|
+
threshold = config.getoption("--mutation-threshold")
|
|
58
|
+
|
|
59
|
+
if not Path(source_dir).exists():
|
|
60
|
+
terminalreporter.write_sep("=", "No source directory found", red=True)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
tester = SmartPolarsTestRunner(test_command="pytest")
|
|
65
|
+
|
|
66
|
+
terminalreporter.write_sep("=", "Mutation Testing Results")
|
|
67
|
+
|
|
68
|
+
for py_file in Path(source_dir).rglob("*.py"):
|
|
69
|
+
results = tester.analyze_mutation_efficiency(str(py_file))
|
|
70
|
+
mutations = results['high_value_mutations']
|
|
71
|
+
false_pos = results['potential_false_positives_avoided']
|
|
72
|
+
|
|
73
|
+
if mutations >= 1: # Only show if mutations found
|
|
74
|
+
status = "✅" if mutations > 0 else "⚠️"
|
|
75
|
+
terminalreporter.write(f"\n{status} {py_file}: {mutations} mutations ({false_pos:.1f}% false pos avoided)")
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
terminalreporter.write_sep("!", f"Mutation testing error: {e}", yellow=True)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Report generation and export functionality."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
from .config import MutationConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaselineTracker:
|
|
11
|
+
"""Track and compare mutation testing baselines."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: MutationConfig):
|
|
14
|
+
self.config = config
|
|
15
|
+
|
|
16
|
+
def save_baseline(self, results: Dict[str, Any]) -> None:
|
|
17
|
+
"""Save current results as baseline."""
|
|
18
|
+
baseline_data = {
|
|
19
|
+
"timestamp": datetime.now().isoformat(),
|
|
20
|
+
"results": results
|
|
21
|
+
}
|
|
22
|
+
Path(self.config.baseline_path).write_text(json.dumps(baseline_data, indent=2))
|
|
23
|
+
|
|
24
|
+
def load_baseline(self) -> Dict[str, Any]:
|
|
25
|
+
"""Load saved baseline."""
|
|
26
|
+
path = Path(self.config.baseline_path)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
data = json.loads(path.read_text())
|
|
31
|
+
return data.get("results", {})
|
|
32
|
+
|
|
33
|
+
def compare(self, current: Dict[str, Any]) -> Dict[str, Any]:
|
|
34
|
+
"""Compare current results to baseline."""
|
|
35
|
+
baseline = self.load_baseline()
|
|
36
|
+
comparison = {}
|
|
37
|
+
|
|
38
|
+
for file, results in current.items():
|
|
39
|
+
baseline_val = baseline.get(file, {})
|
|
40
|
+
comparison[file] = {
|
|
41
|
+
"current": results.get("high_value_mutations", 0),
|
|
42
|
+
"baseline": baseline_val.get("high_value_mutations", 0),
|
|
43
|
+
"change": results.get("high_value_mutations", 0) - baseline_val.get("high_value_mutations", 0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return comparison
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class JSONExporter:
|
|
50
|
+
"""Export mutation testing results to JSON."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: MutationConfig):
|
|
53
|
+
self.config = config
|
|
54
|
+
|
|
55
|
+
def export(self, results: Dict[str, Any], output_path: str) -> None:
|
|
56
|
+
"""Export results to JSON file."""
|
|
57
|
+
export_data = {
|
|
58
|
+
"timestamp": datetime.now().isoformat(),
|
|
59
|
+
"config": self.config.to_dict(),
|
|
60
|
+
"results": results,
|
|
61
|
+
"summary": self._generate_summary(results)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Path(output_path).write_text(json.dumps(export_data, indent=2))
|
|
65
|
+
|
|
66
|
+
def _generate_summary(self, results: Dict[str, Any]) -> Dict[str, Any]:
|
|
67
|
+
"""Generate summary statistics."""
|
|
68
|
+
total_mutations = sum(r.get("high_value_mutations", 0) for r in results.values())
|
|
69
|
+
avg_false_pos_avoided = sum(
|
|
70
|
+
r.get("potential_false_positives_avoided", 0)
|
|
71
|
+
for r in results.values()
|
|
72
|
+
) / len(results) if results else 0
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"total_files": len(results),
|
|
76
|
+
"total_mutations": total_mutations,
|
|
77
|
+
"avg_false_positives_avoided": round(avg_false_pos_avoided, 2),
|
|
78
|
+
"timestamp": datetime.now().isoformat()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class HTMLReportGenerator:
|
|
83
|
+
"""Generate HTML reports with visualizations."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, config: MutationConfig):
|
|
86
|
+
self.config = config
|
|
87
|
+
|
|
88
|
+
def generate(self, results: Dict[str, Any], output_path: str) -> None:
|
|
89
|
+
"""Generate HTML report."""
|
|
90
|
+
html = self._build_html(results)
|
|
91
|
+
Path(output_path).write_text(html, encoding="utf-8")
|
|
92
|
+
|
|
93
|
+
def _build_html(self, results: Dict[str, Any]) -> str:
|
|
94
|
+
"""Build HTML content."""
|
|
95
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
96
|
+
|
|
97
|
+
# Generate rows
|
|
98
|
+
rows = ""
|
|
99
|
+
for file, data in results.items():
|
|
100
|
+
mutations = data.get("high_value_mutations", 0)
|
|
101
|
+
false_pos = data.get("potential_false_positives_avoided", 0)
|
|
102
|
+
rows += f"""
|
|
103
|
+
<tr>
|
|
104
|
+
<td>{file}</td>
|
|
105
|
+
<td>{mutations}</td>
|
|
106
|
+
<td>{false_pos:.1f}%</td>
|
|
107
|
+
</tr>
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
total_mutations = sum(d.get("high_value_mutations", 0) for d in results.values())
|
|
111
|
+
avg_false_pos = sum(d.get("potential_false_positives_avoided", 0) for d in results.values()) / len(results) if results else 0
|
|
112
|
+
|
|
113
|
+
return f"""
|
|
114
|
+
<!DOCTYPE html>
|
|
115
|
+
<html>
|
|
116
|
+
<head>
|
|
117
|
+
<title>Mutation Testing Report</title>
|
|
118
|
+
<meta charset="utf-8">
|
|
119
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
120
|
+
<style>
|
|
121
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
122
|
+
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #f5f5f5; padding: 20px; }}
|
|
123
|
+
.container {{ max-width: 1000px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
|
|
124
|
+
h1 {{ color: #333; margin-bottom: 10px; }}
|
|
125
|
+
.timestamp {{ color: #999; font-size: 14px; margin-bottom: 30px; }}
|
|
126
|
+
.metrics {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }}
|
|
127
|
+
.metric {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; }}
|
|
128
|
+
.metric-value {{ font-size: 28px; font-weight: bold; }}
|
|
129
|
+
.metric-label {{ font-size: 12px; opacity: 0.9; margin-top: 5px; }}
|
|
130
|
+
table {{ width: 100%; border-collapse: collapse; }}
|
|
131
|
+
th {{ background: #f5f5f5; padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #ddd; }}
|
|
132
|
+
td {{ padding: 12px; border-bottom: 1px solid #eee; }}
|
|
133
|
+
tr:hover {{ background: #f9f9f9; }}
|
|
134
|
+
.badge {{ display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }}
|
|
135
|
+
.badge-success {{ background: #c6f6d5; color: #22543d; }}
|
|
136
|
+
.badge-warning {{ background: #fed7d7; color: #742a2a; }}
|
|
137
|
+
footer {{ margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: #999; font-size: 12px; }}
|
|
138
|
+
</style>
|
|
139
|
+
</head>
|
|
140
|
+
<body>
|
|
141
|
+
<div class="container">
|
|
142
|
+
<h1>🧬 Mutation Testing Report</h1>
|
|
143
|
+
<div class="timestamp">Generated on {timestamp}</div>
|
|
144
|
+
|
|
145
|
+
<div class="metrics">
|
|
146
|
+
<div class="metric">
|
|
147
|
+
<div class="metric-value">{total_mutations}</div>
|
|
148
|
+
<div class="metric-label">Total Mutations</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="metric">
|
|
151
|
+
<div class="metric-value">{len(results)}</div>
|
|
152
|
+
<div class="metric-label">Files Analyzed</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="metric">
|
|
155
|
+
<div class="metric-value">{avg_false_pos:.1f}%</div>
|
|
156
|
+
<div class="metric-label">False Positives Avoided</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<h2 style="margin-bottom: 15px; color: #333;">Results by File</h2>
|
|
161
|
+
<table>
|
|
162
|
+
<thead>
|
|
163
|
+
<tr>
|
|
164
|
+
<th>File</th>
|
|
165
|
+
<th>Mutations</th>
|
|
166
|
+
<th>False Positives Avoided</th>
|
|
167
|
+
</tr>
|
|
168
|
+
</thead>
|
|
169
|
+
<tbody>
|
|
170
|
+
{rows}
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
173
|
+
|
|
174
|
+
<footer>
|
|
175
|
+
<p>Generated by dataframe-mutator v0.1.1</p>
|
|
176
|
+
<p>6-10x faster mutation testing with smart AST-aware filtering</p>
|
|
177
|
+
</footer>
|
|
178
|
+
</div>
|
|
179
|
+
</body>
|
|
180
|
+
</html>
|
|
181
|
+
"""
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Tests for advanced features."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
# Import new features
|
|
9
|
+
from dataframe_mutator.config import MutationConfig
|
|
10
|
+
from dataframe_mutator.reports import BaselineTracker, JSONExporter, HTMLReportGenerator
|
|
11
|
+
from dataframe_mutator.integrations import (
|
|
12
|
+
CustomOperatorRegistry, OperatorSuggester, SlackNotifier
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestMutationConfig:
|
|
17
|
+
"""Test configuration system."""
|
|
18
|
+
|
|
19
|
+
def test_config_creation(self):
|
|
20
|
+
"""Test creating default config."""
|
|
21
|
+
config = MutationConfig()
|
|
22
|
+
assert config.mutation_threshold == 0.85
|
|
23
|
+
assert config.parallel == True
|
|
24
|
+
assert config.output_format == "text"
|
|
25
|
+
|
|
26
|
+
def test_config_to_dict(self):
|
|
27
|
+
"""Test config serialization."""
|
|
28
|
+
config = MutationConfig(mutation_threshold=0.9)
|
|
29
|
+
data = config.to_dict()
|
|
30
|
+
assert data["mutation_threshold"] == 0.9
|
|
31
|
+
|
|
32
|
+
def test_config_json_save_load(self):
|
|
33
|
+
"""Test JSON config save/load."""
|
|
34
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
35
|
+
path = f"{tmpdir}/config.json"
|
|
36
|
+
|
|
37
|
+
config = MutationConfig(mutation_threshold=0.95)
|
|
38
|
+
config.to_json(path)
|
|
39
|
+
|
|
40
|
+
loaded = MutationConfig.from_json(path)
|
|
41
|
+
assert loaded.mutation_threshold == 0.95
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestBaselineTracker:
|
|
45
|
+
"""Test baseline tracking."""
|
|
46
|
+
|
|
47
|
+
def test_save_and_load_baseline(self):
|
|
48
|
+
"""Test saving and loading baseline."""
|
|
49
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
50
|
+
config = MutationConfig(baseline_path=f"{tmpdir}/baseline.json")
|
|
51
|
+
tracker = BaselineTracker(config)
|
|
52
|
+
|
|
53
|
+
results = {
|
|
54
|
+
"file.py": {"high_value_mutations": 10}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tracker.save_baseline(results)
|
|
58
|
+
loaded = tracker.load_baseline()
|
|
59
|
+
|
|
60
|
+
assert loaded["file.py"]["high_value_mutations"] == 10
|
|
61
|
+
|
|
62
|
+
def test_compare_results(self):
|
|
63
|
+
"""Test comparing current to baseline."""
|
|
64
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
65
|
+
config = MutationConfig(baseline_path=f"{tmpdir}/baseline.json")
|
|
66
|
+
tracker = BaselineTracker(config)
|
|
67
|
+
|
|
68
|
+
baseline = {"file.py": {"high_value_mutations": 10}}
|
|
69
|
+
tracker.save_baseline(baseline)
|
|
70
|
+
|
|
71
|
+
current = {"file.py": {"high_value_mutations": 12}}
|
|
72
|
+
comparison = tracker.compare(current)
|
|
73
|
+
|
|
74
|
+
assert comparison["file.py"]["change"] == 2
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestJSONExporter:
|
|
78
|
+
"""Test JSON export."""
|
|
79
|
+
|
|
80
|
+
def test_export_json(self):
|
|
81
|
+
"""Test exporting to JSON."""
|
|
82
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
83
|
+
path = f"{tmpdir}/results.json"
|
|
84
|
+
config = MutationConfig()
|
|
85
|
+
exporter = JSONExporter(config)
|
|
86
|
+
|
|
87
|
+
results = {
|
|
88
|
+
"file.py": {
|
|
89
|
+
"high_value_mutations": 10,
|
|
90
|
+
"potential_false_positives_avoided": 98.5
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exporter.export(results, path)
|
|
95
|
+
|
|
96
|
+
with open(path) as f:
|
|
97
|
+
data = json.load(f)
|
|
98
|
+
|
|
99
|
+
assert data["results"]["file.py"]["high_value_mutations"] == 10
|
|
100
|
+
assert data["summary"]["total_mutations"] == 10
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestHTMLReportGenerator:
|
|
104
|
+
"""Test HTML report generation."""
|
|
105
|
+
|
|
106
|
+
def test_generate_html_report(self):
|
|
107
|
+
"""Test HTML report generation."""
|
|
108
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
109
|
+
path = f"{tmpdir}/report.html"
|
|
110
|
+
config = MutationConfig()
|
|
111
|
+
generator = HTMLReportGenerator(config)
|
|
112
|
+
|
|
113
|
+
results = {
|
|
114
|
+
"file.py": {
|
|
115
|
+
"high_value_mutations": 10,
|
|
116
|
+
"potential_false_positives_avoided": 98.5
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
generator.generate(results, path)
|
|
121
|
+
|
|
122
|
+
html = Path(path).read_text()
|
|
123
|
+
assert "<html>" in html.lower()
|
|
124
|
+
assert "mutation" in html.lower()
|
|
125
|
+
assert "10" in html
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestOperatorSuggester:
|
|
129
|
+
"""Test operator suggestions."""
|
|
130
|
+
|
|
131
|
+
def test_suggest_operators_for_code(self):
|
|
132
|
+
"""Test suggesting operators for code."""
|
|
133
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
134
|
+
# Create test file
|
|
135
|
+
test_file = f"{tmpdir}/test.py"
|
|
136
|
+
Path(test_file).write_text("""
|
|
137
|
+
df.filter(pl.col('amount') > 0)
|
|
138
|
+
df.sum()
|
|
139
|
+
df.join(other)
|
|
140
|
+
""")
|
|
141
|
+
|
|
142
|
+
suggestions = OperatorSuggester.analyze_code(test_file)
|
|
143
|
+
|
|
144
|
+
assert "filter_mutations" in suggestions
|
|
145
|
+
assert "aggregation_mutations" in suggestions
|
|
146
|
+
assert "join_mutations" in suggestions
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestCustomOperatorRegistry:
|
|
150
|
+
"""Test custom operator registry."""
|
|
151
|
+
|
|
152
|
+
def test_registry_creation(self):
|
|
153
|
+
"""Test creating operator registry."""
|
|
154
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
155
|
+
registry = CustomOperatorRegistry(tmpdir)
|
|
156
|
+
assert Path(tmpdir).exists()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestSlackNotifier:
|
|
160
|
+
"""Test Slack integration."""
|
|
161
|
+
|
|
162
|
+
def test_notifier_creation(self):
|
|
163
|
+
"""Test creating Slack notifier."""
|
|
164
|
+
notifier = SlackNotifier("https://hooks.slack.com/test")
|
|
165
|
+
assert notifier.webhook_url == "https://hooks.slack.com/test"
|
|
166
|
+
|
|
167
|
+
def test_notify_does_not_error(self):
|
|
168
|
+
"""Test notification doesn't crash on invalid webhook."""
|
|
169
|
+
notifier = SlackNotifier("https://invalid.example.com")
|
|
170
|
+
|
|
171
|
+
results = {"file.py": {"high_value_mutations": 10}}
|
|
172
|
+
# Should not raise
|
|
173
|
+
notifier.notify(results)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
pytest.main([__file__, "-v"])
|
|
File without changes
|
|
File without changes
|
{dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/dataframe_mutator.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{dataframe_mutator-0.1.1 → dataframe_mutator-0.2.0}/dataframe_mutator.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|