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,807 @@
|
|
|
1
|
+
"""Verification pipeline orchestrator for Serenecode.
|
|
2
|
+
|
|
3
|
+
This module orchestrates the sequential execution of verification
|
|
4
|
+
levels (1→2→3→4→5→6), handling early termination, result merging,
|
|
5
|
+
and level selection.
|
|
6
|
+
|
|
7
|
+
This is a core module — no I/O operations are permitted. All
|
|
8
|
+
verification backends are injected through protocol interfaces.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
import icontract
|
|
18
|
+
|
|
19
|
+
from serenecode.config import SerenecodeConfig
|
|
20
|
+
from serenecode.contracts.predicates import is_non_empty_string, is_positive_int, is_valid_verification_level
|
|
21
|
+
from serenecode.core.exceptions import ToolNotInstalledError
|
|
22
|
+
from serenecode.ports.coverage_analyzer import CoverageAnalyzer
|
|
23
|
+
from serenecode.ports.property_tester import PropertyTester
|
|
24
|
+
from serenecode.ports.symbolic_checker import SymbolicChecker, SymbolicFinding
|
|
25
|
+
from serenecode.ports.type_checker import TypeChecker
|
|
26
|
+
from serenecode.models import (
|
|
27
|
+
CheckResult,
|
|
28
|
+
CheckStatus,
|
|
29
|
+
Detail,
|
|
30
|
+
FunctionResult,
|
|
31
|
+
VerificationLevel,
|
|
32
|
+
make_check_result,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
StructuralChecker = Callable[[str, SerenecodeConfig, str, str], CheckResult]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@icontract.invariant(
|
|
39
|
+
lambda self: len(self.file_path) > 0,
|
|
40
|
+
"Source file must have a non-empty file path",
|
|
41
|
+
)
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SourceFile:
|
|
44
|
+
"""A source file to be verified.
|
|
45
|
+
|
|
46
|
+
Contains the file path, derived module path, and source content.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
file_path: str
|
|
50
|
+
module_path: str
|
|
51
|
+
source: str
|
|
52
|
+
importable_module: str | None = None # e.g. "tests.fixtures.valid.simple_function"
|
|
53
|
+
import_search_paths: tuple[str, ...] = ()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@icontract.require(
|
|
57
|
+
lambda level: is_valid_verification_level(level),
|
|
58
|
+
"level must be between 1 and 6",
|
|
59
|
+
)
|
|
60
|
+
@icontract.require(
|
|
61
|
+
lambda start_level: is_valid_verification_level(start_level),
|
|
62
|
+
"start_level must be between 1 and 6",
|
|
63
|
+
)
|
|
64
|
+
@icontract.require(
|
|
65
|
+
lambda level, start_level: start_level <= level,
|
|
66
|
+
"start_level must not exceed level",
|
|
67
|
+
)
|
|
68
|
+
@icontract.require(
|
|
69
|
+
lambda max_workers: is_positive_int(max_workers),
|
|
70
|
+
"max_workers must be at least 1",
|
|
71
|
+
)
|
|
72
|
+
@icontract.ensure(
|
|
73
|
+
lambda result: isinstance(result, CheckResult),
|
|
74
|
+
"result must be a CheckResult",
|
|
75
|
+
)
|
|
76
|
+
def run_pipeline(
|
|
77
|
+
source_files: tuple[SourceFile, ...],
|
|
78
|
+
level: int,
|
|
79
|
+
start_level: int,
|
|
80
|
+
config: SerenecodeConfig,
|
|
81
|
+
structural_checker: StructuralChecker | None = None,
|
|
82
|
+
type_checker: TypeChecker | None = None,
|
|
83
|
+
coverage_analyzer: CoverageAnalyzer | None = None,
|
|
84
|
+
property_tester: PropertyTester | None = None,
|
|
85
|
+
symbolic_checker: SymbolicChecker | None = None,
|
|
86
|
+
early_termination: bool = True,
|
|
87
|
+
progress: Callable[[str], None] | None = None,
|
|
88
|
+
max_workers: int = 4,
|
|
89
|
+
) -> CheckResult:
|
|
90
|
+
"""Run the verification pipeline up to the specified level.
|
|
91
|
+
|
|
92
|
+
Executes levels sequentially (1→2→3→4→5→6). If early_termination
|
|
93
|
+
is True (default), stops at the first level with failures.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
source_files: Tuple of source files to verify.
|
|
97
|
+
level: Maximum verification level (1-6).
|
|
98
|
+
start_level: First verification level to execute.
|
|
99
|
+
config: Active Serenecode configuration.
|
|
100
|
+
structural_checker: Callable for Level 1 (or None to use default).
|
|
101
|
+
type_checker: TypeChecker protocol implementation for Level 2.
|
|
102
|
+
coverage_analyzer: CoverageAnalyzer protocol implementation for Level 3.
|
|
103
|
+
property_tester: PropertyTester protocol implementation for Level 4.
|
|
104
|
+
symbolic_checker: SymbolicChecker protocol implementation for Level 5.
|
|
105
|
+
early_termination: Stop at first failing level if True.
|
|
106
|
+
progress: Optional callback for progress messages.
|
|
107
|
+
max_workers: Max concurrent modules for Level 5 symbolic verification.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
An aggregated CheckResult across all executed levels.
|
|
111
|
+
"""
|
|
112
|
+
start_time = time.monotonic()
|
|
113
|
+
all_results: list[FunctionResult] = []
|
|
114
|
+
achieved_level = start_level - 1
|
|
115
|
+
has_source_files = len(source_files) > 0
|
|
116
|
+
|
|
117
|
+
# Cap max_workers to a reasonable limit to avoid resource exhaustion
|
|
118
|
+
max_workers = min(max_workers, 32)
|
|
119
|
+
|
|
120
|
+
def _emit(msg: str) -> None:
|
|
121
|
+
if progress is not None:
|
|
122
|
+
try:
|
|
123
|
+
progress(msg)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass # Never let progress callback failures abort the pipeline
|
|
126
|
+
|
|
127
|
+
# Level 1: Structural check
|
|
128
|
+
if start_level <= 1 <= level:
|
|
129
|
+
_emit(f"Level 1: Structural check ({len(source_files)} files)...")
|
|
130
|
+
level_1_results = _run_level_1(source_files, config, structural_checker)
|
|
131
|
+
all_results.extend(level_1_results)
|
|
132
|
+
|
|
133
|
+
if early_termination and _has_failures(level_1_results):
|
|
134
|
+
elapsed = time.monotonic() - start_time
|
|
135
|
+
return make_check_result(
|
|
136
|
+
tuple(all_results),
|
|
137
|
+
level_requested=level,
|
|
138
|
+
duration_seconds=elapsed,
|
|
139
|
+
level_achieved=achieved_level,
|
|
140
|
+
)
|
|
141
|
+
if _level_achieved(level_1_results, has_source_files):
|
|
142
|
+
achieved_level = 1
|
|
143
|
+
|
|
144
|
+
# Level 2: Type checking
|
|
145
|
+
if start_level <= 2 <= level:
|
|
146
|
+
if type_checker is not None:
|
|
147
|
+
_emit("Level 2: Type checking...")
|
|
148
|
+
level_2_results = _run_level_2(source_files, type_checker)
|
|
149
|
+
else:
|
|
150
|
+
_emit("Level 2: Type checking unavailable.")
|
|
151
|
+
level_2_results = _make_unavailable_results(
|
|
152
|
+
source_files,
|
|
153
|
+
requested_level=2,
|
|
154
|
+
level_achieved=1,
|
|
155
|
+
tool="mypy",
|
|
156
|
+
message="Type checking unavailable: mypy is not installed",
|
|
157
|
+
)
|
|
158
|
+
all_results.extend(level_2_results)
|
|
159
|
+
|
|
160
|
+
if early_termination and _has_failures(level_2_results):
|
|
161
|
+
elapsed = time.monotonic() - start_time
|
|
162
|
+
return make_check_result(
|
|
163
|
+
tuple(all_results),
|
|
164
|
+
level_requested=level,
|
|
165
|
+
duration_seconds=elapsed,
|
|
166
|
+
level_achieved=achieved_level,
|
|
167
|
+
)
|
|
168
|
+
if _level_achieved(level_2_results, has_source_files):
|
|
169
|
+
achieved_level = 2
|
|
170
|
+
|
|
171
|
+
# Level 3: Coverage analysis
|
|
172
|
+
if start_level <= 3 <= level:
|
|
173
|
+
if coverage_analyzer is not None:
|
|
174
|
+
_emit("Level 3: Coverage analysis...")
|
|
175
|
+
level_3_results = _run_level_3_coverage(source_files, coverage_analyzer)
|
|
176
|
+
else:
|
|
177
|
+
_emit("Level 3: Coverage analysis unavailable.")
|
|
178
|
+
level_3_results = _make_unavailable_results(
|
|
179
|
+
source_files,
|
|
180
|
+
requested_level=3,
|
|
181
|
+
level_achieved=2,
|
|
182
|
+
tool="coverage",
|
|
183
|
+
message="Coverage analysis unavailable: coverage is not installed",
|
|
184
|
+
)
|
|
185
|
+
all_results.extend(level_3_results)
|
|
186
|
+
|
|
187
|
+
if early_termination and _has_failures(level_3_results):
|
|
188
|
+
elapsed = time.monotonic() - start_time
|
|
189
|
+
return make_check_result(
|
|
190
|
+
tuple(all_results),
|
|
191
|
+
level_requested=level,
|
|
192
|
+
duration_seconds=elapsed,
|
|
193
|
+
level_achieved=achieved_level,
|
|
194
|
+
)
|
|
195
|
+
if _level_achieved(level_3_results, has_source_files, require_evidence=True):
|
|
196
|
+
achieved_level = 3
|
|
197
|
+
|
|
198
|
+
# Level 4: Property-based testing
|
|
199
|
+
if start_level <= 4 <= level:
|
|
200
|
+
if property_tester is not None:
|
|
201
|
+
_emit("Level 4: Property-based testing...")
|
|
202
|
+
level_4_results = _run_level_4(source_files, property_tester)
|
|
203
|
+
else:
|
|
204
|
+
_emit("Level 4: Property-based testing unavailable.")
|
|
205
|
+
level_4_results = _make_unavailable_results(
|
|
206
|
+
source_files,
|
|
207
|
+
requested_level=4,
|
|
208
|
+
level_achieved=3,
|
|
209
|
+
tool="hypothesis",
|
|
210
|
+
message="Property testing unavailable: Hypothesis is not installed",
|
|
211
|
+
)
|
|
212
|
+
all_results.extend(level_4_results)
|
|
213
|
+
|
|
214
|
+
if early_termination and _has_failures(level_4_results):
|
|
215
|
+
elapsed = time.monotonic() - start_time
|
|
216
|
+
return make_check_result(
|
|
217
|
+
tuple(all_results),
|
|
218
|
+
level_requested=level,
|
|
219
|
+
duration_seconds=elapsed,
|
|
220
|
+
level_achieved=achieved_level,
|
|
221
|
+
)
|
|
222
|
+
if _level_achieved(level_4_results, has_source_files, require_evidence=True):
|
|
223
|
+
achieved_level = 4
|
|
224
|
+
|
|
225
|
+
# Level 5: Symbolic verification
|
|
226
|
+
if start_level <= 5 <= level:
|
|
227
|
+
if symbolic_checker is not None:
|
|
228
|
+
_emit("Level 5: Symbolic verification (this may take several minutes)...")
|
|
229
|
+
level_5_results = _run_level_5(source_files, symbolic_checker, _emit, max_workers)
|
|
230
|
+
else:
|
|
231
|
+
_emit("Level 5: Symbolic verification unavailable.")
|
|
232
|
+
level_5_results = _make_unavailable_results(
|
|
233
|
+
source_files,
|
|
234
|
+
requested_level=5,
|
|
235
|
+
level_achieved=4,
|
|
236
|
+
tool="crosshair",
|
|
237
|
+
message="Symbolic verification unavailable: CrossHair is not installed",
|
|
238
|
+
)
|
|
239
|
+
all_results.extend(level_5_results)
|
|
240
|
+
|
|
241
|
+
if early_termination and _has_failures(level_5_results):
|
|
242
|
+
elapsed = time.monotonic() - start_time
|
|
243
|
+
return make_check_result(
|
|
244
|
+
tuple(all_results),
|
|
245
|
+
level_requested=level,
|
|
246
|
+
duration_seconds=elapsed,
|
|
247
|
+
level_achieved=achieved_level,
|
|
248
|
+
)
|
|
249
|
+
if _level_achieved(level_5_results, has_source_files, require_evidence=True):
|
|
250
|
+
achieved_level = 5
|
|
251
|
+
|
|
252
|
+
# Level 6: Compositional verification
|
|
253
|
+
if start_level <= 6 <= level:
|
|
254
|
+
_emit("Level 6: Compositional verification...")
|
|
255
|
+
level_6_results = _run_level_6(source_files, config)
|
|
256
|
+
all_results.extend(level_6_results)
|
|
257
|
+
if _level_achieved(level_6_results, has_source_files):
|
|
258
|
+
achieved_level = 6
|
|
259
|
+
|
|
260
|
+
elapsed = time.monotonic() - start_time
|
|
261
|
+
return make_check_result(
|
|
262
|
+
tuple(all_results),
|
|
263
|
+
level_requested=level,
|
|
264
|
+
duration_seconds=elapsed,
|
|
265
|
+
level_achieved=achieved_level,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@icontract.require(
|
|
270
|
+
lambda results: isinstance(results, list),
|
|
271
|
+
"results must be a list",
|
|
272
|
+
)
|
|
273
|
+
@icontract.ensure(
|
|
274
|
+
lambda result: isinstance(result, bool),
|
|
275
|
+
"result must be a bool",
|
|
276
|
+
)
|
|
277
|
+
def _has_failures(results: list[FunctionResult]) -> bool:
|
|
278
|
+
"""Check if any results indicate failure.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
results: List of function results to check.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
True if any result has FAILED status.
|
|
285
|
+
"""
|
|
286
|
+
# Loop invariant: result is True if any of results[0..i] has FAILED status
|
|
287
|
+
for r in results:
|
|
288
|
+
if r.status == CheckStatus.FAILED:
|
|
289
|
+
return True
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@icontract.require(
|
|
294
|
+
lambda results: isinstance(results, list),
|
|
295
|
+
"results must be a list",
|
|
296
|
+
)
|
|
297
|
+
@icontract.ensure(
|
|
298
|
+
lambda result: isinstance(result, bool),
|
|
299
|
+
"result must be a bool",
|
|
300
|
+
)
|
|
301
|
+
def _has_skips(results: list[FunctionResult]) -> bool:
|
|
302
|
+
"""Check if any results indicate an incomplete verification step."""
|
|
303
|
+
# Loop invariant: result is True if any of results[0..i] has SKIPPED status
|
|
304
|
+
for r in results:
|
|
305
|
+
if r.status == CheckStatus.SKIPPED:
|
|
306
|
+
return True
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@icontract.require(
|
|
311
|
+
lambda results: isinstance(results, list),
|
|
312
|
+
"results must be a list",
|
|
313
|
+
)
|
|
314
|
+
@icontract.ensure(
|
|
315
|
+
lambda result: isinstance(result, bool),
|
|
316
|
+
"result must be a bool",
|
|
317
|
+
)
|
|
318
|
+
def _level_achieved(
|
|
319
|
+
results: list[FunctionResult],
|
|
320
|
+
has_source_files: bool,
|
|
321
|
+
require_evidence: bool = False,
|
|
322
|
+
) -> bool:
|
|
323
|
+
"""Check if a verification level was achieved.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
results: Level results to evaluate.
|
|
327
|
+
has_source_files: Whether the pipeline had any source files.
|
|
328
|
+
require_evidence: If True, empty results with source files
|
|
329
|
+
means the level is NOT achieved (used for L3/L4/L5 where
|
|
330
|
+
empty results means no functions were exercised). If False,
|
|
331
|
+
empty results means "no issues found" which counts as a pass
|
|
332
|
+
(used for L1/L2/L6 where the checker examines all files).
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
True if the level should be considered achieved.
|
|
336
|
+
"""
|
|
337
|
+
if _has_failures(results) or _has_skips(results):
|
|
338
|
+
return False
|
|
339
|
+
if not has_source_files:
|
|
340
|
+
return True
|
|
341
|
+
if require_evidence and not results:
|
|
342
|
+
return False
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@icontract.require(
|
|
347
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
348
|
+
"source_files must be a tuple",
|
|
349
|
+
)
|
|
350
|
+
@icontract.require(
|
|
351
|
+
lambda requested_level: requested_level in (2, 3, 4, 5),
|
|
352
|
+
"requested_level must be a backend verification level",
|
|
353
|
+
)
|
|
354
|
+
@icontract.require(
|
|
355
|
+
lambda level_achieved: 0 <= level_achieved <= 5,
|
|
356
|
+
"level_achieved must be within the completed pipeline range",
|
|
357
|
+
)
|
|
358
|
+
@icontract.require(
|
|
359
|
+
lambda tool: is_non_empty_string(tool),
|
|
360
|
+
"tool must be a non-empty string",
|
|
361
|
+
)
|
|
362
|
+
@icontract.require(
|
|
363
|
+
lambda message: is_non_empty_string(message),
|
|
364
|
+
"message must be a non-empty string",
|
|
365
|
+
)
|
|
366
|
+
@icontract.ensure(
|
|
367
|
+
lambda result: isinstance(result, list),
|
|
368
|
+
"result must be a list",
|
|
369
|
+
)
|
|
370
|
+
def _make_unavailable_results(
|
|
371
|
+
source_files: tuple[SourceFile, ...],
|
|
372
|
+
requested_level: int,
|
|
373
|
+
level_achieved: int,
|
|
374
|
+
tool: str,
|
|
375
|
+
message: str,
|
|
376
|
+
) -> list[FunctionResult]:
|
|
377
|
+
"""Create per-file skipped results when a verification backend is unavailable."""
|
|
378
|
+
level_map = {
|
|
379
|
+
2: VerificationLevel.TYPES,
|
|
380
|
+
3: VerificationLevel.COVERAGE,
|
|
381
|
+
4: VerificationLevel.PROPERTIES,
|
|
382
|
+
5: VerificationLevel.SYMBOLIC,
|
|
383
|
+
}
|
|
384
|
+
verification_level = level_map[requested_level]
|
|
385
|
+
results: list[FunctionResult] = []
|
|
386
|
+
|
|
387
|
+
# Loop invariant: results contains unavailable-backend findings for source_files[0..i]
|
|
388
|
+
for sf in source_files:
|
|
389
|
+
results.append(FunctionResult(
|
|
390
|
+
function="<module>",
|
|
391
|
+
file=sf.file_path,
|
|
392
|
+
line=1,
|
|
393
|
+
level_requested=requested_level,
|
|
394
|
+
level_achieved=level_achieved,
|
|
395
|
+
status=CheckStatus.SKIPPED,
|
|
396
|
+
details=(Detail(
|
|
397
|
+
level=verification_level,
|
|
398
|
+
tool=tool,
|
|
399
|
+
finding_type="unavailable",
|
|
400
|
+
message=message,
|
|
401
|
+
),),
|
|
402
|
+
))
|
|
403
|
+
|
|
404
|
+
return results
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@icontract.require(
|
|
408
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
409
|
+
"source_files must be a tuple",
|
|
410
|
+
)
|
|
411
|
+
@icontract.require(
|
|
412
|
+
lambda config: isinstance(config, SerenecodeConfig),
|
|
413
|
+
"config must be a SerenecodeConfig",
|
|
414
|
+
)
|
|
415
|
+
@icontract.ensure(
|
|
416
|
+
lambda result: isinstance(result, list),
|
|
417
|
+
"result must be a list",
|
|
418
|
+
)
|
|
419
|
+
def _run_level_1(
|
|
420
|
+
source_files: tuple[SourceFile, ...],
|
|
421
|
+
config: SerenecodeConfig,
|
|
422
|
+
structural_checker: StructuralChecker | None = None,
|
|
423
|
+
) -> list[FunctionResult]:
|
|
424
|
+
"""Run Level 1 structural checks on all source files.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
source_files: Files to check.
|
|
428
|
+
config: Active configuration.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
List of function results from structural checking.
|
|
432
|
+
"""
|
|
433
|
+
checker = structural_checker
|
|
434
|
+
if checker is None:
|
|
435
|
+
from serenecode.checker.structural import check_structural
|
|
436
|
+
|
|
437
|
+
checker = check_structural
|
|
438
|
+
|
|
439
|
+
results: list[FunctionResult] = []
|
|
440
|
+
# Loop invariant: results contains structural check results for source_files[0..i]
|
|
441
|
+
for sf in source_files:
|
|
442
|
+
check_result = checker(
|
|
443
|
+
sf.source, config, sf.module_path, sf.file_path,
|
|
444
|
+
)
|
|
445
|
+
results.extend(check_result.results)
|
|
446
|
+
return results
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@icontract.require(
|
|
450
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
451
|
+
"source_files must be a tuple",
|
|
452
|
+
)
|
|
453
|
+
@icontract.require(
|
|
454
|
+
lambda type_checker: type_checker is not None,
|
|
455
|
+
"type_checker must be provided",
|
|
456
|
+
)
|
|
457
|
+
@icontract.ensure(
|
|
458
|
+
lambda result: isinstance(result, list),
|
|
459
|
+
"result must be a list",
|
|
460
|
+
)
|
|
461
|
+
def _run_level_2(
|
|
462
|
+
source_files: tuple[SourceFile, ...],
|
|
463
|
+
type_checker: TypeChecker,
|
|
464
|
+
) -> list[FunctionResult]:
|
|
465
|
+
"""Run Level 2 type checking on source files.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
source_files: Files to check.
|
|
469
|
+
type_checker: TypeChecker protocol implementation.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
List of function results from type checking.
|
|
473
|
+
"""
|
|
474
|
+
from serenecode.checker.types import transform_type_results
|
|
475
|
+
from serenecode.ports.type_checker import TypeIssue
|
|
476
|
+
|
|
477
|
+
file_paths = [sf.file_path for sf in source_files]
|
|
478
|
+
search_paths = _collect_type_check_search_paths(source_files)
|
|
479
|
+
issues: list[TypeIssue] = type_checker.check(
|
|
480
|
+
file_paths,
|
|
481
|
+
search_paths=search_paths,
|
|
482
|
+
)
|
|
483
|
+
return list(transform_type_results(issues, 0.0).results)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@icontract.require(
|
|
487
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
488
|
+
"source_files must be a tuple",
|
|
489
|
+
)
|
|
490
|
+
@icontract.ensure(
|
|
491
|
+
lambda result: isinstance(result, tuple),
|
|
492
|
+
"result must be a tuple",
|
|
493
|
+
)
|
|
494
|
+
def _collect_type_check_search_paths(
|
|
495
|
+
source_files: tuple[SourceFile, ...],
|
|
496
|
+
) -> tuple[str, ...]:
|
|
497
|
+
"""Collect unique import roots needed for static type checking."""
|
|
498
|
+
search_paths: list[str] = []
|
|
499
|
+
|
|
500
|
+
# Loop invariant: search_paths contains unique import roots from source_files[0..i]
|
|
501
|
+
for sf in source_files:
|
|
502
|
+
# Loop invariant: search_paths contains unique roots from sf.import_search_paths[0..j]
|
|
503
|
+
for path in sf.import_search_paths:
|
|
504
|
+
if path not in search_paths:
|
|
505
|
+
search_paths.append(path)
|
|
506
|
+
|
|
507
|
+
return tuple(search_paths)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@icontract.require(
|
|
511
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
512
|
+
"source_files must be a tuple",
|
|
513
|
+
)
|
|
514
|
+
@icontract.require(
|
|
515
|
+
lambda coverage_analyzer: coverage_analyzer is not None,
|
|
516
|
+
"coverage_analyzer must be provided",
|
|
517
|
+
)
|
|
518
|
+
@icontract.ensure(
|
|
519
|
+
lambda result: isinstance(result, list),
|
|
520
|
+
"result must be a list",
|
|
521
|
+
)
|
|
522
|
+
def _run_level_3_coverage(
|
|
523
|
+
source_files: tuple[SourceFile, ...],
|
|
524
|
+
coverage_analyzer: CoverageAnalyzer,
|
|
525
|
+
) -> list[FunctionResult]:
|
|
526
|
+
"""Run Level 3 coverage analysis on source files.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
source_files: Files to check.
|
|
530
|
+
coverage_analyzer: CoverageAnalyzer protocol implementation.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
List of function results from coverage analysis.
|
|
534
|
+
"""
|
|
535
|
+
from serenecode.checker.coverage import transform_coverage_results
|
|
536
|
+
|
|
537
|
+
results: list[FunctionResult] = []
|
|
538
|
+
# Loop invariant: results contains coverage findings for source_files[0..i]
|
|
539
|
+
for sf in source_files:
|
|
540
|
+
if sf.importable_module is None:
|
|
541
|
+
results.append(FunctionResult(
|
|
542
|
+
function="<module>",
|
|
543
|
+
file=sf.file_path,
|
|
544
|
+
line=1,
|
|
545
|
+
level_requested=3,
|
|
546
|
+
level_achieved=2,
|
|
547
|
+
status=CheckStatus.SKIPPED,
|
|
548
|
+
details=(Detail(
|
|
549
|
+
level=VerificationLevel.COVERAGE,
|
|
550
|
+
tool="coverage",
|
|
551
|
+
finding_type="not_importable",
|
|
552
|
+
message=f"Module '{sf.file_path}' is not importable as a Python package",
|
|
553
|
+
),),
|
|
554
|
+
))
|
|
555
|
+
continue
|
|
556
|
+
try:
|
|
557
|
+
findings = coverage_analyzer.analyze_module(
|
|
558
|
+
sf.importable_module,
|
|
559
|
+
search_paths=sf.import_search_paths,
|
|
560
|
+
)
|
|
561
|
+
check_result = transform_coverage_results(findings, sf.file_path, 0.0)
|
|
562
|
+
results.extend(check_result.results)
|
|
563
|
+
except Exception as exc:
|
|
564
|
+
results.append(FunctionResult(
|
|
565
|
+
function="<module>",
|
|
566
|
+
file=sf.file_path,
|
|
567
|
+
line=1,
|
|
568
|
+
level_requested=3,
|
|
569
|
+
level_achieved=2,
|
|
570
|
+
status=CheckStatus.SKIPPED,
|
|
571
|
+
details=(Detail(
|
|
572
|
+
level=VerificationLevel.COVERAGE,
|
|
573
|
+
tool="coverage",
|
|
574
|
+
finding_type="unavailable" if isinstance(exc, ToolNotInstalledError) else "error",
|
|
575
|
+
message=f"Coverage analysis skipped for '{sf.importable_module}': {exc}",
|
|
576
|
+
),),
|
|
577
|
+
))
|
|
578
|
+
return results
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@icontract.require(
|
|
582
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
583
|
+
"source_files must be a tuple",
|
|
584
|
+
)
|
|
585
|
+
@icontract.require(
|
|
586
|
+
lambda property_tester: property_tester is not None,
|
|
587
|
+
"property_tester must be provided",
|
|
588
|
+
)
|
|
589
|
+
@icontract.ensure(
|
|
590
|
+
lambda result: isinstance(result, list),
|
|
591
|
+
"result must be a list",
|
|
592
|
+
)
|
|
593
|
+
def _run_level_4(
|
|
594
|
+
source_files: tuple[SourceFile, ...],
|
|
595
|
+
property_tester: PropertyTester,
|
|
596
|
+
) -> list[FunctionResult]:
|
|
597
|
+
"""Run Level 4 property-based testing on source files.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
source_files: Files to check.
|
|
601
|
+
property_tester: PropertyTester protocol implementation.
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
List of function results from property testing.
|
|
605
|
+
"""
|
|
606
|
+
from serenecode.checker.properties import transform_property_results
|
|
607
|
+
|
|
608
|
+
results: list[FunctionResult] = []
|
|
609
|
+
# Loop invariant: results contains property test results for source_files[0..i]
|
|
610
|
+
for sf in source_files:
|
|
611
|
+
if sf.importable_module is None:
|
|
612
|
+
results.append(FunctionResult(
|
|
613
|
+
function="<module>",
|
|
614
|
+
file=sf.file_path,
|
|
615
|
+
line=1,
|
|
616
|
+
level_requested=4,
|
|
617
|
+
level_achieved=3,
|
|
618
|
+
status=CheckStatus.SKIPPED,
|
|
619
|
+
details=(Detail(
|
|
620
|
+
level=VerificationLevel.PROPERTIES,
|
|
621
|
+
tool="hypothesis",
|
|
622
|
+
finding_type="not_importable",
|
|
623
|
+
message=f"Module '{sf.file_path}' is not importable as a Python package",
|
|
624
|
+
),),
|
|
625
|
+
))
|
|
626
|
+
continue
|
|
627
|
+
try:
|
|
628
|
+
findings = property_tester.test_module(
|
|
629
|
+
sf.importable_module,
|
|
630
|
+
search_paths=sf.import_search_paths,
|
|
631
|
+
)
|
|
632
|
+
check_result = transform_property_results(findings, sf.file_path, 0.0)
|
|
633
|
+
results.extend(check_result.results)
|
|
634
|
+
except Exception as exc:
|
|
635
|
+
# Record the error as a skipped result rather than silently dropping it
|
|
636
|
+
results.append(FunctionResult(
|
|
637
|
+
function="<module>",
|
|
638
|
+
file=sf.file_path,
|
|
639
|
+
line=1,
|
|
640
|
+
level_requested=4,
|
|
641
|
+
level_achieved=3,
|
|
642
|
+
status=CheckStatus.SKIPPED,
|
|
643
|
+
details=(Detail(
|
|
644
|
+
level=VerificationLevel.PROPERTIES,
|
|
645
|
+
tool="hypothesis",
|
|
646
|
+
finding_type="unavailable" if isinstance(exc, ToolNotInstalledError) else "error",
|
|
647
|
+
message=f"Property testing skipped for '{sf.importable_module}': {exc}",
|
|
648
|
+
),),
|
|
649
|
+
))
|
|
650
|
+
return results
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@icontract.require(
|
|
654
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
655
|
+
"source_files must be a tuple",
|
|
656
|
+
)
|
|
657
|
+
@icontract.require(
|
|
658
|
+
lambda symbolic_checker: symbolic_checker is not None,
|
|
659
|
+
"symbolic_checker must be provided",
|
|
660
|
+
)
|
|
661
|
+
@icontract.require(
|
|
662
|
+
lambda emit: emit is not None,
|
|
663
|
+
"emit callback must be provided",
|
|
664
|
+
)
|
|
665
|
+
@icontract.require(
|
|
666
|
+
lambda max_workers: is_positive_int(max_workers),
|
|
667
|
+
"max_workers must be at least 1",
|
|
668
|
+
)
|
|
669
|
+
@icontract.ensure(
|
|
670
|
+
lambda result: isinstance(result, list),
|
|
671
|
+
"result must be a list",
|
|
672
|
+
)
|
|
673
|
+
def _run_level_5(
|
|
674
|
+
source_files: tuple[SourceFile, ...],
|
|
675
|
+
symbolic_checker: SymbolicChecker,
|
|
676
|
+
emit: Callable[[str], None] = lambda _msg: None,
|
|
677
|
+
max_workers: int = 4,
|
|
678
|
+
) -> list[FunctionResult]:
|
|
679
|
+
"""Run Level 5 symbolic verification on source files in parallel.
|
|
680
|
+
|
|
681
|
+
Each module is verified in its own subprocess (via the symbolic
|
|
682
|
+
checker adapter), and we dispatch multiple modules concurrently
|
|
683
|
+
using a thread pool.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
source_files: Files to check.
|
|
687
|
+
symbolic_checker: SymbolicChecker protocol implementation.
|
|
688
|
+
emit: Callback for progress messages.
|
|
689
|
+
max_workers: Maximum number of modules to verify concurrently.
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
List of function results from symbolic verification.
|
|
693
|
+
"""
|
|
694
|
+
import concurrent.futures
|
|
695
|
+
|
|
696
|
+
from serenecode.checker.symbolic import transform_symbolic_results
|
|
697
|
+
|
|
698
|
+
verifiable: list[SourceFile] = []
|
|
699
|
+
results: list[FunctionResult] = []
|
|
700
|
+
|
|
701
|
+
# Record non-importable modules as skipped so they appear in the output
|
|
702
|
+
# Loop invariant: verifiable contains importable files from source_files[0..i]
|
|
703
|
+
for sf in source_files:
|
|
704
|
+
if sf.importable_module is not None:
|
|
705
|
+
verifiable.append(sf)
|
|
706
|
+
else:
|
|
707
|
+
results.append(FunctionResult(
|
|
708
|
+
function="<module>",
|
|
709
|
+
file=sf.file_path,
|
|
710
|
+
line=1,
|
|
711
|
+
level_requested=5,
|
|
712
|
+
level_achieved=4,
|
|
713
|
+
status=CheckStatus.SKIPPED,
|
|
714
|
+
details=(Detail(
|
|
715
|
+
level=VerificationLevel.SYMBOLIC,
|
|
716
|
+
tool="crosshair",
|
|
717
|
+
finding_type="not_importable",
|
|
718
|
+
message=f"Module '{sf.file_path}' is not importable as a Python package",
|
|
719
|
+
),),
|
|
720
|
+
))
|
|
721
|
+
|
|
722
|
+
total = len(verifiable)
|
|
723
|
+
completed = 0
|
|
724
|
+
|
|
725
|
+
def _verify_one(
|
|
726
|
+
sf: SourceFile,
|
|
727
|
+
) -> tuple[SourceFile, list[SymbolicFinding] | None, Exception | None]:
|
|
728
|
+
if sf.importable_module is None:
|
|
729
|
+
return (sf, None, ToolNotInstalledError("No importable module"))
|
|
730
|
+
try:
|
|
731
|
+
findings = symbolic_checker.verify_module(
|
|
732
|
+
sf.importable_module,
|
|
733
|
+
search_paths=sf.import_search_paths,
|
|
734
|
+
)
|
|
735
|
+
return (sf, findings, None)
|
|
736
|
+
except Exception as exc:
|
|
737
|
+
return (sf, None, exc)
|
|
738
|
+
|
|
739
|
+
emit(f" Verifying {total} modules ({max_workers} workers)...")
|
|
740
|
+
|
|
741
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
742
|
+
futures = {executor.submit(_verify_one, sf): sf for sf in verifiable}
|
|
743
|
+
# Loop invariant: results contains findings for all completed futures
|
|
744
|
+
for future in concurrent.futures.as_completed(futures):
|
|
745
|
+
completed += 1
|
|
746
|
+
sf, findings, error = future.result()
|
|
747
|
+
module_name = sf.importable_module
|
|
748
|
+
if error is not None:
|
|
749
|
+
emit(f" [{completed}/{total}] Skipped {module_name}: {error}")
|
|
750
|
+
results.append(FunctionResult(
|
|
751
|
+
function="<module>",
|
|
752
|
+
file=sf.file_path,
|
|
753
|
+
line=1,
|
|
754
|
+
level_requested=5,
|
|
755
|
+
level_achieved=4,
|
|
756
|
+
status=CheckStatus.SKIPPED if isinstance(error, ToolNotInstalledError) else CheckStatus.FAILED,
|
|
757
|
+
details=(Detail(
|
|
758
|
+
level=VerificationLevel.SYMBOLIC,
|
|
759
|
+
tool="crosshair",
|
|
760
|
+
finding_type="unavailable" if isinstance(error, ToolNotInstalledError) else "error",
|
|
761
|
+
message=f"Symbolic verification skipped for '{module_name}': {error}"
|
|
762
|
+
if isinstance(error, ToolNotInstalledError)
|
|
763
|
+
else f"Symbolic verification failed for '{module_name}': {error}",
|
|
764
|
+
),),
|
|
765
|
+
))
|
|
766
|
+
elif findings is not None:
|
|
767
|
+
emit(f" [{completed}/{total}] Done {module_name}")
|
|
768
|
+
check_result = transform_symbolic_results(findings, sf.file_path, 0.0)
|
|
769
|
+
results.extend(check_result.results)
|
|
770
|
+
|
|
771
|
+
return results
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@icontract.require(
|
|
775
|
+
lambda source_files: isinstance(source_files, tuple),
|
|
776
|
+
"source_files must be a tuple",
|
|
777
|
+
)
|
|
778
|
+
@icontract.require(
|
|
779
|
+
lambda config: isinstance(config, SerenecodeConfig),
|
|
780
|
+
"config must be a SerenecodeConfig",
|
|
781
|
+
)
|
|
782
|
+
@icontract.ensure(
|
|
783
|
+
lambda result: isinstance(result, list),
|
|
784
|
+
"result must be a list",
|
|
785
|
+
)
|
|
786
|
+
def _run_level_6(
|
|
787
|
+
source_files: tuple[SourceFile, ...],
|
|
788
|
+
config: SerenecodeConfig,
|
|
789
|
+
) -> list[FunctionResult]:
|
|
790
|
+
"""Run Level 6 compositional verification across all source files.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
source_files: Files to check.
|
|
794
|
+
config: Active configuration.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
List of function results from compositional checking.
|
|
798
|
+
"""
|
|
799
|
+
from serenecode.checker.compositional import check_compositional
|
|
800
|
+
|
|
801
|
+
sources: list[tuple[str, str, str]] = []
|
|
802
|
+
# Loop invariant: sources contains (source, file_path, module_path) for source_files[0..i]
|
|
803
|
+
for sf in source_files:
|
|
804
|
+
sources.append((sf.source, sf.file_path, sf.module_path))
|
|
805
|
+
|
|
806
|
+
result = check_compositional(sources, config)
|
|
807
|
+
return list(result.results)
|