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,124 @@
|
|
|
1
|
+
"""Port definition for coverage analysis (Level 3).
|
|
2
|
+
|
|
3
|
+
This module defines the Protocol interface for coverage analysis
|
|
4
|
+
backends using coverage.py. The checker module depends on this
|
|
5
|
+
protocol rather than concrete implementations.
|
|
6
|
+
|
|
7
|
+
This is a ports module — no implementations, only abstract contracts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import icontract
|
|
16
|
+
|
|
17
|
+
from serenecode.contracts.predicates import is_non_empty_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@icontract.invariant(
|
|
21
|
+
lambda self: is_non_empty_string(self.name),
|
|
22
|
+
"dependency name must be non-empty",
|
|
23
|
+
)
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class MockDependency:
|
|
26
|
+
"""A dependency that must be mocked for a test to reach uncovered code.
|
|
27
|
+
|
|
28
|
+
Represents a call target found via AST analysis in an uncovered path.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
import_module: str
|
|
33
|
+
is_external: bool
|
|
34
|
+
mock_necessary: bool
|
|
35
|
+
reason: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@icontract.invariant(
|
|
39
|
+
lambda self: is_non_empty_string(self.description),
|
|
40
|
+
"test suggestion description must be non-empty",
|
|
41
|
+
)
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class TestSuggestion:
|
|
44
|
+
"""A suggested test to cover an uncovered code path.
|
|
45
|
+
|
|
46
|
+
Includes the mock setup code needed and whether each mock
|
|
47
|
+
could be replaced with a real implementation.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
description: str
|
|
51
|
+
target_lines: tuple[int, ...]
|
|
52
|
+
suggested_test_code: str
|
|
53
|
+
required_mocks: tuple[MockDependency, ...]
|
|
54
|
+
all_mocks_necessary: bool
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@icontract.invariant(
|
|
58
|
+
lambda self: is_non_empty_string(self.function_name)
|
|
59
|
+
and is_non_empty_string(self.module_path),
|
|
60
|
+
"finding names must be non-empty",
|
|
61
|
+
)
|
|
62
|
+
@icontract.invariant(
|
|
63
|
+
lambda self: 0.0 <= self.line_coverage_percent <= 100.0,
|
|
64
|
+
"line coverage must be a valid percentage",
|
|
65
|
+
)
|
|
66
|
+
@icontract.invariant(
|
|
67
|
+
lambda self: 0.0 <= self.branch_coverage_percent <= 100.0,
|
|
68
|
+
"branch coverage must be a valid percentage",
|
|
69
|
+
)
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class CoverageFinding:
|
|
72
|
+
"""A coverage analysis finding for a single function.
|
|
73
|
+
|
|
74
|
+
Contains coverage metrics, uncovered locations, test suggestions
|
|
75
|
+
with mock assessments, and the threshold pass/fail determination.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
function_name: str
|
|
79
|
+
module_path: str
|
|
80
|
+
line_start: int
|
|
81
|
+
line_end: int
|
|
82
|
+
line_coverage_percent: float
|
|
83
|
+
branch_coverage_percent: float
|
|
84
|
+
uncovered_lines: tuple[int, ...]
|
|
85
|
+
uncovered_branches: tuple[tuple[int, int], ...]
|
|
86
|
+
suggestions: tuple[TestSuggestion, ...]
|
|
87
|
+
meets_threshold: bool
|
|
88
|
+
message: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Protocol classes are exempt from @icontract.invariant — see ports/file_system.py.
|
|
92
|
+
class CoverageAnalyzer(Protocol):
|
|
93
|
+
"""Port for coverage analysis.
|
|
94
|
+
|
|
95
|
+
Implementations use coverage.py and AST analysis to measure
|
|
96
|
+
test coverage per function and generate test suggestions for
|
|
97
|
+
uncovered paths.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@icontract.require(
|
|
101
|
+
lambda module_path: is_non_empty_string(module_path),
|
|
102
|
+
"module_path must be a non-empty string",
|
|
103
|
+
)
|
|
104
|
+
@icontract.ensure(
|
|
105
|
+
lambda result: isinstance(result, list),
|
|
106
|
+
"result must be a list",
|
|
107
|
+
)
|
|
108
|
+
def analyze_module(
|
|
109
|
+
self,
|
|
110
|
+
module_path: str,
|
|
111
|
+
search_paths: tuple[str, ...] = (),
|
|
112
|
+
coverage_threshold: float = 80.0,
|
|
113
|
+
) -> list[CoverageFinding]:
|
|
114
|
+
"""Run coverage analysis on all functions in a module.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
module_path: Importable Python module path to analyze.
|
|
118
|
+
search_paths: sys.path roots needed to import the module.
|
|
119
|
+
coverage_threshold: Minimum coverage percentage to pass.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of coverage findings per function.
|
|
123
|
+
"""
|
|
124
|
+
...
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Port definitions for file system operations.
|
|
2
|
+
|
|
3
|
+
This module defines the Protocol interfaces for reading and writing
|
|
4
|
+
files. Core modules depend on these protocols rather than concrete
|
|
5
|
+
file system implementations, enabling dependency injection and
|
|
6
|
+
testability.
|
|
7
|
+
|
|
8
|
+
This is a ports module — no implementations, only abstract contracts.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import icontract
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Protocol classes are exempt from @icontract.invariant because invariants
|
|
19
|
+
# are not inherited by Protocol implementors — they would only fire on the
|
|
20
|
+
# Protocol itself, which is never instantiated directly.
|
|
21
|
+
class FileReader(Protocol):
|
|
22
|
+
"""Port for reading file contents.
|
|
23
|
+
|
|
24
|
+
Implementations must handle encoding and raise domain exceptions
|
|
25
|
+
for file system errors.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@icontract.require(lambda path: isinstance(path, str), "path must be a string")
|
|
29
|
+
@icontract.ensure(lambda result: isinstance(result, str), "result must be a string")
|
|
30
|
+
def read_file(self, path: str) -> str:
|
|
31
|
+
"""Read a file and return its contents as a UTF-8 string.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
path: Path to the file to read.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The full file contents as a string.
|
|
38
|
+
"""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
@icontract.require(lambda path: isinstance(path, str), "path must be a string")
|
|
42
|
+
@icontract.ensure(lambda result: isinstance(result, bool), "result must be a bool")
|
|
43
|
+
def file_exists(self, path: str) -> bool:
|
|
44
|
+
"""Check whether a file exists at the given path.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: Path to check.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if a file exists at path.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@icontract.require(lambda directory: isinstance(directory, str), "directory must be a string")
|
|
55
|
+
@icontract.ensure(lambda result: isinstance(result, list), "result must be a list")
|
|
56
|
+
def list_python_files(self, directory: str) -> list[str]:
|
|
57
|
+
"""List all Python (.py) files in a directory recursively.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
directory: Root directory to search.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of absolute or relative paths to .py files.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FileWriter(Protocol):
|
|
69
|
+
"""Port for writing file contents.
|
|
70
|
+
|
|
71
|
+
Implementations must handle encoding and raise domain exceptions
|
|
72
|
+
for file system errors.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@icontract.require(lambda path: isinstance(path, str), "path must be a string")
|
|
76
|
+
@icontract.require(lambda content: isinstance(content, str), "content must be a string")
|
|
77
|
+
@icontract.ensure(lambda result: result is None, "result must be None")
|
|
78
|
+
def write_file(self, path: str, content: str) -> None:
|
|
79
|
+
"""Write content to a file, creating it if it doesn't exist.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
path: Path to the file to write.
|
|
83
|
+
content: Content to write as UTF-8.
|
|
84
|
+
"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
@icontract.require(lambda path: isinstance(path, str), "path must be a string")
|
|
88
|
+
@icontract.ensure(lambda result: result is None, "result must be None")
|
|
89
|
+
def ensure_directory(self, path: str) -> None:
|
|
90
|
+
"""Ensure a directory exists, creating it if necessary.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path: Path to the directory.
|
|
94
|
+
"""
|
|
95
|
+
...
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Port definition for property-based testing (Level 3).
|
|
2
|
+
|
|
3
|
+
This module defines the Protocol interface for property-based testing
|
|
4
|
+
backends such as Hypothesis. The checker module depends on this
|
|
5
|
+
protocol rather than concrete implementations.
|
|
6
|
+
|
|
7
|
+
This is a ports module — no implementations, only abstract contracts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import icontract
|
|
16
|
+
|
|
17
|
+
from serenecode.contracts.predicates import is_non_empty_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@icontract.invariant(
|
|
22
|
+
lambda self: is_non_empty_string(self.function_name) and is_non_empty_string(self.module_path),
|
|
23
|
+
"finding names must be non-empty",
|
|
24
|
+
)
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class PropertyFinding:
|
|
27
|
+
"""A single finding from property-based testing.
|
|
28
|
+
|
|
29
|
+
Represents either a successful verification or a counterexample
|
|
30
|
+
that violates a postcondition.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
function_name: str
|
|
34
|
+
module_path: str
|
|
35
|
+
passed: bool
|
|
36
|
+
finding_type: str # "postcondition_violated", "precondition_error", "crash", "verified"
|
|
37
|
+
message: str
|
|
38
|
+
counterexample: dict[str, object] | None = None
|
|
39
|
+
exception_type: str | None = None
|
|
40
|
+
exception_message: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Protocol classes are exempt from @icontract.invariant — see ports/file_system.py.
|
|
44
|
+
class PropertyTester(Protocol):
|
|
45
|
+
"""Port for property-based testing.
|
|
46
|
+
|
|
47
|
+
Implementations use Hypothesis (or similar) to generate test inputs
|
|
48
|
+
from function contracts and verify postconditions hold.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@icontract.require(lambda module_path: is_non_empty_string(module_path), "module_path must be a non-empty string")
|
|
52
|
+
@icontract.ensure(lambda result: isinstance(result, list), "result must be a list")
|
|
53
|
+
def test_module(
|
|
54
|
+
self,
|
|
55
|
+
module_path: str,
|
|
56
|
+
max_examples: int | None = None,
|
|
57
|
+
search_paths: tuple[str, ...] = (),
|
|
58
|
+
) -> list[PropertyFinding]:
|
|
59
|
+
"""Run property-based tests on all contracted functions in a module.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
module_path: Importable Python module path to test.
|
|
63
|
+
max_examples: Maximum number of test examples per function.
|
|
64
|
+
search_paths: sys.path roots needed to import the module.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of property findings.
|
|
68
|
+
"""
|
|
69
|
+
...
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Port definition for symbolic verification (Level 4).
|
|
2
|
+
|
|
3
|
+
This module defines the Protocol interface for symbolic execution
|
|
4
|
+
backends such as CrossHair. The checker module depends on this
|
|
5
|
+
protocol rather than concrete implementations.
|
|
6
|
+
|
|
7
|
+
This is a ports module — no implementations, only abstract contracts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import icontract
|
|
16
|
+
|
|
17
|
+
from serenecode.contracts.predicates import is_non_empty_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@icontract.invariant(
|
|
22
|
+
lambda self: is_non_empty_string(self.function_name) and is_non_empty_string(self.module_path),
|
|
23
|
+
"finding names must be non-empty",
|
|
24
|
+
)
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class SymbolicFinding:
|
|
27
|
+
"""A single finding from symbolic verification.
|
|
28
|
+
|
|
29
|
+
Represents one of three outcomes: verified (proven correct),
|
|
30
|
+
counterexample (violation found), or unknown (timeout/unsupported).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
function_name: str
|
|
34
|
+
module_path: str
|
|
35
|
+
outcome: str # "verified", "counterexample", "timeout", "unsupported", "error"
|
|
36
|
+
message: str
|
|
37
|
+
counterexample: dict[str, object] | None = None
|
|
38
|
+
condition: str | None = None # which postcondition was violated
|
|
39
|
+
duration_seconds: float = 0.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Protocol classes are exempt from @icontract.invariant — see ports/file_system.py.
|
|
43
|
+
class SymbolicChecker(Protocol):
|
|
44
|
+
"""Port for symbolic verification.
|
|
45
|
+
|
|
46
|
+
Implementations use CrossHair/Z3 (or similar) to symbolically
|
|
47
|
+
execute functions and prove contracts hold for all valid inputs.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@icontract.require(lambda module_path: is_non_empty_string(module_path), "module_path must be a non-empty string")
|
|
51
|
+
@icontract.ensure(lambda result: isinstance(result, list), "result must be a list")
|
|
52
|
+
def verify_module(
|
|
53
|
+
self,
|
|
54
|
+
module_path: str,
|
|
55
|
+
per_condition_timeout: int | None = None,
|
|
56
|
+
per_path_timeout: int | None = None,
|
|
57
|
+
search_paths: tuple[str, ...] = (),
|
|
58
|
+
) -> list[SymbolicFinding]:
|
|
59
|
+
"""Run symbolic verification on all contracted functions in a module.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
module_path: Importable Python module path to verify.
|
|
63
|
+
per_condition_timeout: Max seconds per postcondition.
|
|
64
|
+
per_path_timeout: Max seconds per execution path.
|
|
65
|
+
search_paths: sys.path roots needed to import the module.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of symbolic findings.
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Port definition for type checking (Level 2).
|
|
2
|
+
|
|
3
|
+
This module defines the Protocol interface for static type analysis
|
|
4
|
+
backends such as mypy. The checker module depends on this protocol
|
|
5
|
+
rather than concrete implementations.
|
|
6
|
+
|
|
7
|
+
This is a ports module — no implementations, only abstract contracts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import icontract
|
|
16
|
+
|
|
17
|
+
from serenecode.contracts.predicates import is_non_empty_string
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@icontract.invariant(
|
|
22
|
+
lambda self: is_non_empty_string(self.file) and self.line >= 0 and self.column >= 0,
|
|
23
|
+
"type issues must reference a file and non-negative position",
|
|
24
|
+
)
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class TypeIssue:
|
|
27
|
+
"""A single type checking issue reported by a backend like mypy.
|
|
28
|
+
|
|
29
|
+
Represents one error, warning, or note from the type checker.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
file: str
|
|
33
|
+
line: int
|
|
34
|
+
column: int
|
|
35
|
+
severity: str # "error", "warning", "note"
|
|
36
|
+
message: str
|
|
37
|
+
code: str | None = None # e.g. "arg-type", "return-value"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Protocol classes are exempt from @icontract.invariant — see ports/file_system.py.
|
|
41
|
+
class TypeChecker(Protocol):
|
|
42
|
+
"""Port for static type checking.
|
|
43
|
+
|
|
44
|
+
Implementations run a type checker (e.g. mypy) on source files
|
|
45
|
+
and return structured issue reports.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@icontract.require(lambda file_paths: isinstance(file_paths, list), "file_paths must be a list")
|
|
49
|
+
@icontract.ensure(lambda result: isinstance(result, list), "result must be a list")
|
|
50
|
+
def check(
|
|
51
|
+
self,
|
|
52
|
+
file_paths: list[str],
|
|
53
|
+
strict: bool = True,
|
|
54
|
+
search_paths: tuple[str, ...] = (),
|
|
55
|
+
) -> list[TypeIssue]:
|
|
56
|
+
"""Run type checking on the given files.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
file_paths: Paths to Python files to check.
|
|
60
|
+
strict: Whether to use strict mode.
|
|
61
|
+
search_paths: Import roots needed to resolve local modules.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of type issues found.
|
|
65
|
+
"""
|
|
66
|
+
...
|