serenecode 0.1.0__py3-none-any.whl
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.
- serenecode/__init__.py +281 -0
- serenecode/adapters/__init__.py +6 -0
- serenecode/adapters/coverage_adapter.py +1173 -0
- serenecode/adapters/crosshair_adapter.py +1069 -0
- serenecode/adapters/hypothesis_adapter.py +1824 -0
- serenecode/adapters/local_fs.py +169 -0
- serenecode/adapters/module_loader.py +492 -0
- serenecode/adapters/mypy_adapter.py +161 -0
- serenecode/checker/__init__.py +6 -0
- serenecode/checker/compositional.py +2216 -0
- serenecode/checker/coverage.py +186 -0
- serenecode/checker/properties.py +154 -0
- serenecode/checker/structural.py +1504 -0
- serenecode/checker/symbolic.py +178 -0
- serenecode/checker/types.py +148 -0
- serenecode/cli.py +478 -0
- serenecode/config.py +711 -0
- serenecode/contracts/__init__.py +6 -0
- serenecode/contracts/predicates.py +176 -0
- serenecode/core/__init__.py +6 -0
- serenecode/core/exceptions.py +38 -0
- serenecode/core/pipeline.py +807 -0
- serenecode/init.py +307 -0
- serenecode/models.py +308 -0
- serenecode/ports/__init__.py +6 -0
- serenecode/ports/coverage_analyzer.py +124 -0
- serenecode/ports/file_system.py +95 -0
- serenecode/ports/property_tester.py +69 -0
- serenecode/ports/symbolic_checker.py +70 -0
- serenecode/ports/type_checker.py +66 -0
- serenecode/reporter.py +346 -0
- serenecode/source_discovery.py +319 -0
- serenecode/templates/__init__.py +5 -0
- serenecode/templates/content.py +337 -0
- serenecode-0.1.0.dist-info/METADATA +298 -0
- serenecode-0.1.0.dist-info/RECORD +39 -0
- serenecode-0.1.0.dist-info/WHEEL +4 -0
- serenecode-0.1.0.dist-info/entry_points.txt +2 -0
- serenecode-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Coverage analysis checker for Serenecode (Level 3).
|
|
2
|
+
|
|
3
|
+
This module implements Level 3 verification: it transforms results from
|
|
4
|
+
coverage analysis backends into structured CheckResult objects.
|
|
5
|
+
The actual analysis is delegated to adapters.
|
|
6
|
+
|
|
7
|
+
This is a core module — no I/O operations are permitted. Coverage
|
|
8
|
+
results are received as structured data, not generated here.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import icontract
|
|
14
|
+
|
|
15
|
+
from serenecode.models import (
|
|
16
|
+
CheckResult,
|
|
17
|
+
CheckStatus,
|
|
18
|
+
Detail,
|
|
19
|
+
FunctionResult,
|
|
20
|
+
VerificationLevel,
|
|
21
|
+
make_check_result,
|
|
22
|
+
)
|
|
23
|
+
from serenecode.ports.coverage_analyzer import CoverageFinding
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@icontract.require(
|
|
27
|
+
lambda findings: isinstance(findings, list),
|
|
28
|
+
"findings must be a list",
|
|
29
|
+
)
|
|
30
|
+
@icontract.ensure(
|
|
31
|
+
lambda result: isinstance(result, CheckResult),
|
|
32
|
+
"result must be a CheckResult",
|
|
33
|
+
)
|
|
34
|
+
@icontract.ensure(
|
|
35
|
+
lambda findings, result: len(result.results) == len(findings),
|
|
36
|
+
"output count must match input findings count",
|
|
37
|
+
)
|
|
38
|
+
def transform_coverage_results(
|
|
39
|
+
findings: list[CoverageFinding],
|
|
40
|
+
file_path: str,
|
|
41
|
+
duration_seconds: float,
|
|
42
|
+
) -> CheckResult:
|
|
43
|
+
"""Transform coverage analysis findings into a CheckResult.
|
|
44
|
+
|
|
45
|
+
Maps each CoverageFinding to a FunctionResult with appropriate
|
|
46
|
+
status and detailed suggestions for uncovered paths.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
findings: List of coverage findings from an analysis adapter.
|
|
50
|
+
file_path: Source file path for reporting.
|
|
51
|
+
duration_seconds: How long the analysis took.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A CheckResult containing all coverage analysis results.
|
|
55
|
+
"""
|
|
56
|
+
func_results: list[FunctionResult] = []
|
|
57
|
+
|
|
58
|
+
# Loop invariant: func_results contains transformed results for findings[0..i]
|
|
59
|
+
for finding in findings:
|
|
60
|
+
details: list[Detail] = []
|
|
61
|
+
status: CheckStatus
|
|
62
|
+
level_achieved: int
|
|
63
|
+
|
|
64
|
+
if finding.meets_threshold:
|
|
65
|
+
status = CheckStatus.PASSED
|
|
66
|
+
level_achieved = 3
|
|
67
|
+
details.append(Detail(
|
|
68
|
+
level=VerificationLevel.COVERAGE,
|
|
69
|
+
tool="coverage",
|
|
70
|
+
finding_type="sufficient_coverage",
|
|
71
|
+
message=finding.message,
|
|
72
|
+
))
|
|
73
|
+
else:
|
|
74
|
+
status = CheckStatus.FAILED
|
|
75
|
+
level_achieved = 2
|
|
76
|
+
details.append(Detail(
|
|
77
|
+
level=VerificationLevel.COVERAGE,
|
|
78
|
+
tool="coverage",
|
|
79
|
+
finding_type="insufficient_coverage",
|
|
80
|
+
message=finding.message,
|
|
81
|
+
suggestion=_build_suggestion(finding),
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
func_results.append(FunctionResult(
|
|
85
|
+
function=finding.function_name,
|
|
86
|
+
file=file_path,
|
|
87
|
+
line=finding.line_start,
|
|
88
|
+
level_requested=3,
|
|
89
|
+
level_achieved=level_achieved,
|
|
90
|
+
status=status,
|
|
91
|
+
details=tuple(details),
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
return make_check_result(
|
|
95
|
+
tuple(func_results),
|
|
96
|
+
level_requested=3,
|
|
97
|
+
duration_seconds=duration_seconds,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@icontract.require(
|
|
102
|
+
lambda finding: isinstance(finding, CoverageFinding),
|
|
103
|
+
"finding must be a CoverageFinding",
|
|
104
|
+
)
|
|
105
|
+
@icontract.ensure(
|
|
106
|
+
lambda result: isinstance(result, str) and len(result) > 0,
|
|
107
|
+
"result must be a non-empty string",
|
|
108
|
+
)
|
|
109
|
+
def _build_suggestion(finding: CoverageFinding) -> str:
|
|
110
|
+
"""Build an agent-friendly suggestion string from a coverage finding.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
finding: The coverage finding to generate a suggestion for.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
A detailed suggestion string with coverage info and test suggestions.
|
|
117
|
+
"""
|
|
118
|
+
parts: list[str] = []
|
|
119
|
+
parts.append(
|
|
120
|
+
f"Coverage: {finding.line_coverage_percent:.0f}% lines, "
|
|
121
|
+
f"{finding.branch_coverage_percent:.0f}% branches. "
|
|
122
|
+
f"Uncovered lines: {_format_line_ranges(finding.uncovered_lines)}."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Loop invariant: parts contains formatted info for suggestions[0..i]
|
|
126
|
+
for suggestion in finding.suggestions:
|
|
127
|
+
parts.append(f"\nSuggested test ({suggestion.description}):")
|
|
128
|
+
parts.append(suggestion.suggested_test_code)
|
|
129
|
+
if suggestion.required_mocks:
|
|
130
|
+
mock_parts: list[str] = []
|
|
131
|
+
# Loop invariant: mock_parts contains assessments for mocks[0..j]
|
|
132
|
+
for mock in suggestion.required_mocks:
|
|
133
|
+
necessity = (
|
|
134
|
+
"REQUIRED — external I/O"
|
|
135
|
+
if mock.mock_necessary
|
|
136
|
+
else "OPTIONAL — internal code, consider using real implementation"
|
|
137
|
+
)
|
|
138
|
+
mock_parts.append(f" - mock {mock.name} ({mock.import_module}): {necessity} ({mock.reason})")
|
|
139
|
+
parts.append("Mock assessment:\n" + "\n".join(mock_parts))
|
|
140
|
+
else:
|
|
141
|
+
parts.append("No mocks needed — test can use real implementations.")
|
|
142
|
+
|
|
143
|
+
return "\n".join(parts)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@icontract.require(
|
|
147
|
+
lambda lines: isinstance(lines, tuple)
|
|
148
|
+
and all(isinstance(l, int) and l >= 1 for l in lines),
|
|
149
|
+
"lines must be a tuple of positive integers",
|
|
150
|
+
)
|
|
151
|
+
@icontract.ensure(
|
|
152
|
+
lambda result: isinstance(result, str) and len(result) > 0,
|
|
153
|
+
"result must be a non-empty string",
|
|
154
|
+
)
|
|
155
|
+
@icontract.ensure(
|
|
156
|
+
lambda lines, result: result == "none" if not lines else len(result) > 0,
|
|
157
|
+
"empty input produces 'none', non-empty input produces non-empty output",
|
|
158
|
+
)
|
|
159
|
+
def _format_line_ranges(lines: tuple[int, ...]) -> str:
|
|
160
|
+
"""Format a tuple of line numbers into compact ranges.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
lines: Tuple of positive line numbers (need not be sorted).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
A string like "5-8, 12, 15-20".
|
|
167
|
+
"""
|
|
168
|
+
if not lines:
|
|
169
|
+
return "none"
|
|
170
|
+
sorted_lines = sorted(lines)
|
|
171
|
+
ranges: list[str] = []
|
|
172
|
+
start = sorted_lines[0]
|
|
173
|
+
end = start
|
|
174
|
+
|
|
175
|
+
# Loop invariant: [start..end] is the current contiguous range,
|
|
176
|
+
# ranges contains all completed ranges before sorted_lines[i]
|
|
177
|
+
for line in sorted_lines[1:]:
|
|
178
|
+
if line == end + 1:
|
|
179
|
+
end = line
|
|
180
|
+
else:
|
|
181
|
+
ranges.append(f"{start}-{end}" if start != end else str(start))
|
|
182
|
+
start = line
|
|
183
|
+
end = line
|
|
184
|
+
ranges.append(f"{start}-{end}" if start != end else str(start))
|
|
185
|
+
|
|
186
|
+
return ", ".join(ranges)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Property-based testing checker for Serenecode (Level 3).
|
|
2
|
+
|
|
3
|
+
This module implements Level 3 verification: it transforms results from
|
|
4
|
+
property-based testing backends into structured CheckResult objects.
|
|
5
|
+
The actual test execution is delegated to adapters.
|
|
6
|
+
|
|
7
|
+
This is a core module — no I/O operations are permitted. Test results
|
|
8
|
+
are received as structured data, not generated here.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import icontract
|
|
14
|
+
|
|
15
|
+
from serenecode.models import (
|
|
16
|
+
CheckResult,
|
|
17
|
+
CheckStatus,
|
|
18
|
+
Detail,
|
|
19
|
+
FunctionResult,
|
|
20
|
+
VerificationLevel,
|
|
21
|
+
make_check_result,
|
|
22
|
+
)
|
|
23
|
+
from serenecode.ports.property_tester import PropertyFinding
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@icontract.require(
|
|
27
|
+
lambda findings: isinstance(findings, list),
|
|
28
|
+
"findings must be a list",
|
|
29
|
+
)
|
|
30
|
+
@icontract.ensure(
|
|
31
|
+
lambda result: isinstance(result, CheckResult),
|
|
32
|
+
"result must be a CheckResult",
|
|
33
|
+
)
|
|
34
|
+
def transform_property_results(
|
|
35
|
+
findings: list[PropertyFinding],
|
|
36
|
+
file_path: str,
|
|
37
|
+
duration_seconds: float,
|
|
38
|
+
) -> CheckResult:
|
|
39
|
+
"""Transform property-based testing findings into a CheckResult.
|
|
40
|
+
|
|
41
|
+
Maps each PropertyFinding to a FunctionResult with appropriate
|
|
42
|
+
status and detail information including counterexamples.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
findings: List of property findings from a testing adapter.
|
|
46
|
+
file_path: Source file path for reporting.
|
|
47
|
+
duration_seconds: How long the testing took.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
A CheckResult containing all property testing results.
|
|
51
|
+
"""
|
|
52
|
+
func_results: list[FunctionResult] = []
|
|
53
|
+
|
|
54
|
+
# Loop invariant: func_results contains transformed results for findings[0..i]
|
|
55
|
+
for finding in findings:
|
|
56
|
+
details: list[Detail] = []
|
|
57
|
+
status: CheckStatus
|
|
58
|
+
level_achieved: int
|
|
59
|
+
|
|
60
|
+
if finding.passed and finding.finding_type == "verified":
|
|
61
|
+
status = CheckStatus.PASSED
|
|
62
|
+
level_achieved = 4
|
|
63
|
+
details.append(Detail(
|
|
64
|
+
level=VerificationLevel.PROPERTIES,
|
|
65
|
+
tool="hypothesis",
|
|
66
|
+
finding_type="verified",
|
|
67
|
+
message=finding.message,
|
|
68
|
+
))
|
|
69
|
+
elif finding.passed and finding.finding_type == "excluded":
|
|
70
|
+
# Filter-excluded functions are visible but don't block passing.
|
|
71
|
+
status = CheckStatus.EXEMPT
|
|
72
|
+
level_achieved = 3
|
|
73
|
+
details.append(Detail(
|
|
74
|
+
level=VerificationLevel.PROPERTIES,
|
|
75
|
+
tool="hypothesis",
|
|
76
|
+
finding_type=finding.finding_type,
|
|
77
|
+
message=finding.message,
|
|
78
|
+
))
|
|
79
|
+
elif finding.passed:
|
|
80
|
+
status = CheckStatus.SKIPPED
|
|
81
|
+
level_achieved = 3
|
|
82
|
+
details.append(Detail(
|
|
83
|
+
level=VerificationLevel.PROPERTIES,
|
|
84
|
+
tool="hypothesis",
|
|
85
|
+
finding_type=finding.finding_type,
|
|
86
|
+
message=finding.message,
|
|
87
|
+
))
|
|
88
|
+
else:
|
|
89
|
+
status = CheckStatus.FAILED
|
|
90
|
+
level_achieved = 3
|
|
91
|
+
detail = Detail(
|
|
92
|
+
level=VerificationLevel.PROPERTIES,
|
|
93
|
+
tool="hypothesis",
|
|
94
|
+
finding_type=finding.finding_type,
|
|
95
|
+
message=finding.message,
|
|
96
|
+
counterexample=finding.counterexample,
|
|
97
|
+
suggestion=_suggest_fix(finding),
|
|
98
|
+
)
|
|
99
|
+
details.append(detail)
|
|
100
|
+
|
|
101
|
+
func_results.append(FunctionResult(
|
|
102
|
+
function=finding.function_name,
|
|
103
|
+
file=file_path,
|
|
104
|
+
line=1, # line info not available from property testing
|
|
105
|
+
level_requested=4,
|
|
106
|
+
level_achieved=level_achieved,
|
|
107
|
+
status=status,
|
|
108
|
+
details=tuple(details),
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
return make_check_result(
|
|
112
|
+
tuple(func_results),
|
|
113
|
+
level_requested=4,
|
|
114
|
+
duration_seconds=duration_seconds,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@icontract.require(
|
|
119
|
+
lambda finding: isinstance(finding, PropertyFinding),
|
|
120
|
+
"finding must be a PropertyFinding",
|
|
121
|
+
)
|
|
122
|
+
@icontract.ensure(
|
|
123
|
+
lambda result: result is None or isinstance(result, str),
|
|
124
|
+
"result must be None or a string",
|
|
125
|
+
)
|
|
126
|
+
def _suggest_fix(finding: PropertyFinding) -> str | None:
|
|
127
|
+
"""Generate a fix suggestion from a property finding.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
finding: The property finding to generate a suggestion for.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A suggestion string, or None if no suggestion can be generated.
|
|
134
|
+
"""
|
|
135
|
+
if finding.finding_type == "postcondition_violated":
|
|
136
|
+
if finding.counterexample is not None and isinstance(finding.counterexample, dict) and finding.counterexample:
|
|
137
|
+
inputs = ", ".join(f"{k}={v}" for k, v in finding.counterexample.items())
|
|
138
|
+
return (
|
|
139
|
+
f"Postcondition violated with inputs: {inputs}. "
|
|
140
|
+
"To fix: (1) if these inputs are invalid, add a @icontract.require "
|
|
141
|
+
"precondition to exclude them; (2) if these inputs are valid, fix the "
|
|
142
|
+
"implementation so the postcondition holds"
|
|
143
|
+
)
|
|
144
|
+
return (
|
|
145
|
+
"Postcondition violated. Read the function's @icontract.ensure decorators "
|
|
146
|
+
"and fix the implementation to satisfy them, or narrow inputs with @icontract.require"
|
|
147
|
+
)
|
|
148
|
+
elif finding.finding_type == "crash":
|
|
149
|
+
return (
|
|
150
|
+
f"Function crashed with {finding.exception_type}: {finding.exception_message}. "
|
|
151
|
+
"To fix: (1) add a @icontract.require precondition to reject inputs that "
|
|
152
|
+
"cause this crash, or (2) handle the edge case in the implementation"
|
|
153
|
+
)
|
|
154
|
+
return None
|