sflkit 0.5.6__tar.gz → 1.0.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.
- {sflkit-0.5.6/src/sflkit.egg-info → sflkit-1.0.0}/PKG-INFO +13 -4
- {sflkit-0.5.6 → sflkit-1.0.0}/README.md +10 -1
- {sflkit-0.5.6 → sflkit-1.0.0}/pyproject.toml +5 -2
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/__init__.py +1 -1
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/analyzer.py +115 -12
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/factory.py +324 -41
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/predicate.py +45 -29
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/spectra.py +21 -1
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/suggestion.py +21 -2
- sflkit-1.0.0/src/sflkit/evaluation.py +201 -0
- sflkit-1.0.0/src/sflkit/events/event_file.py +121 -0
- sflkit-1.0.0/src/sflkit/features/handler.py +313 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/features/value.py +38 -1
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/features/vector.py +22 -2
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/java/extract.py +111 -36
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/java/factory.py +363 -69
- sflkit-1.0.0/src/sflkit/language/java/finder.py +97 -0
- sflkit-1.0.0/src/sflkit/language/java/visitor.py +310 -0
- sflkit-1.0.0/src/sflkit/language/language.py +196 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/meta.py +13 -1
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/factory.py +343 -79
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/visitor.py +96 -2
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/model/model.py +29 -3
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/model/parallel.py +11 -6
- sflkit-1.0.0/src/sflkit/model/scope.py +155 -0
- sflkit-1.0.0/src/sflkit/online/__init__.py +57 -0
- sflkit-1.0.0/src/sflkit/online/listener.py +266 -0
- sflkit-1.0.0/src/sflkit/online/plugin.py +187 -0
- sflkit-1.0.0/src/sflkit/online/session.py +349 -0
- sflkit-1.0.0/src/sflkit/online/structure.py +210 -0
- sflkit-1.0.0/src/sflkit/online/tracer.py +1238 -0
- sflkit-1.0.0/src/sflkit/online/tree.py +242 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/runners/__init__.py +3 -0
- sflkit-1.0.0/src/sflkit/runners/defects4j.py +429 -0
- sflkit-1.0.0/src/sflkit/runners/resources/SflkitTestRunner.java +47 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/runners/run.py +180 -31
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/weights/models.py +3 -1
- {sflkit-0.5.6 → sflkit-1.0.0/src/sflkit.egg-info}/PKG-INFO +13 -4
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/SOURCES.txt +12 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/requires.txt +2 -2
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_analyzer.py +45 -0
- sflkit-1.0.0/tests/test_defects4j.py +75 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_evaluation.py +107 -1
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_events.py +13 -3
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_features.py +164 -1
- sflkit-1.0.0/tests/test_instrumentation.py +514 -0
- sflkit-1.0.0/tests/test_java.py +673 -0
- sflkit-1.0.0/tests/test_online.py +741 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_runner.py +69 -1
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_visitors.py +7 -0
- sflkit-0.5.6/src/sflkit/evaluation.py +0 -172
- sflkit-0.5.6/src/sflkit/events/event_file.py +0 -58
- sflkit-0.5.6/src/sflkit/features/handler.py +0 -182
- sflkit-0.5.6/src/sflkit/language/java/finder.py +0 -0
- sflkit-0.5.6/src/sflkit/language/java/visitor.py +0 -171
- sflkit-0.5.6/src/sflkit/language/language.py +0 -94
- sflkit-0.5.6/src/sflkit/model/scope.py +0 -82
- sflkit-0.5.6/tests/test_instrumentation.py +0 -263
- {sflkit-0.5.6 → sflkit-1.0.0}/LICENSE +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/setup.cfg +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/analysis_type.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/mapping.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/similarity.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/cli.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/color.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/config.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/events/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/events/mapping.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/features/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/instrumentation/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/instrumentation/dir_instrumentation.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/instrumentation/file_instrumentation.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/extract.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/finder.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/java/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/extract.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/finder.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/visitor.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/logger.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/model/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/weights/__init__.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/weights/analyzer.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/dependency_links.txt +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/entry_points.txt +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/top_level.txt +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_analysis_objects.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_cli.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_color.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_config.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_coverage.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_dependency_weights.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_execution.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_expression.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_language.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_parallelization.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_predicates.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_runner_parallel.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_runner_pytest_versions.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_scope.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_spectra.py +0 -0
- {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_suggestions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sflkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: SFLKit: : A Workbench for Statistical Fault Localization
|
|
5
5
|
Author-email: Marius Smytzek <marius.smytzek@cispa.de>
|
|
6
6
|
Project-URL: Homepage, https://github.com/smythi93/sflkit/
|
|
@@ -15,12 +15,12 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: sflkitlib>=0.2.
|
|
18
|
+
Requires-Dist: sflkitlib>=0.2.4
|
|
19
19
|
Requires-Dist: numpy>=2.1.3
|
|
20
20
|
Requires-Dist: matplotlib>=3.10.0
|
|
21
21
|
Requires-Dist: pandas>=2.2.3
|
|
22
22
|
Requires-Dist: sortedcollections>=2.1.0
|
|
23
|
-
Requires-Dist: java-ast>=1.0.
|
|
23
|
+
Requires-Dist: java-ast>=1.0.3
|
|
24
24
|
Provides-Extra: test
|
|
25
25
|
Requires-Dist: pytest>=8.3.4; extra == "test"
|
|
26
26
|
Requires-Dist: pytest-cov>=6.0.0; extra == "test"
|
|
@@ -45,7 +45,16 @@ whether individual lines are part of the execution.
|
|
|
45
45
|
|
|
46
46
|
## Language Support
|
|
47
47
|
|
|
48
|
-
SFLKit supports
|
|
48
|
+
SFLKit supports Python 3 and Java.
|
|
49
|
+
|
|
50
|
+
Python is instrumented with the standard `ast` module. Events can also be collected
|
|
51
|
+
without instrumenting anything, by tracing the program as it runs; see
|
|
52
|
+
`sflkit.online`.
|
|
53
|
+
|
|
54
|
+
Java is instrumented with [jast](https://github.com/smythi93/jast) and traced by the
|
|
55
|
+
`jsflkit` runtime, which produces the same events as the Python side, so every
|
|
56
|
+
spectrum, predicate and metric applies unchanged. A runner for
|
|
57
|
+
[Defects4J](https://github.com/rjust/defects4j) collects traces per test.
|
|
49
58
|
|
|
50
59
|
## Installation
|
|
51
60
|
|
|
@@ -14,7 +14,16 @@ whether individual lines are part of the execution.
|
|
|
14
14
|
|
|
15
15
|
## Language Support
|
|
16
16
|
|
|
17
|
-
SFLKit supports
|
|
17
|
+
SFLKit supports Python 3 and Java.
|
|
18
|
+
|
|
19
|
+
Python is instrumented with the standard `ast` module. Events can also be collected
|
|
20
|
+
without instrumenting anything, by tracing the program as it runs; see
|
|
21
|
+
`sflkit.online`.
|
|
22
|
+
|
|
23
|
+
Java is instrumented with [jast](https://github.com/smythi93/jast) and traced by the
|
|
24
|
+
`jsflkit` runtime, which produces the same events as the Python side, so every
|
|
25
|
+
spectrum, predicate and metric applies unchanged. A runner for
|
|
26
|
+
[Defects4J](https://github.com/rjust/defects4j) collects traces per test.
|
|
18
27
|
|
|
19
28
|
## Installation
|
|
20
29
|
|
|
@@ -25,12 +25,12 @@ classifiers = [
|
|
|
25
25
|
"Topic :: Software Development :: Testing"
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"sflkitlib>=0.2.
|
|
28
|
+
"sflkitlib>=0.2.4",
|
|
29
29
|
"numpy>=2.1.3",
|
|
30
30
|
"matplotlib>=3.10.0",
|
|
31
31
|
"pandas>=2.2.3",
|
|
32
32
|
"sortedcollections>=2.1.0",
|
|
33
|
-
"java-ast>=1.0.
|
|
33
|
+
"java-ast>=1.0.3",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[project.urls]
|
|
@@ -47,6 +47,9 @@ sflkit = "sflkit.cli:main"
|
|
|
47
47
|
[tool.setuptools.packages.find]
|
|
48
48
|
where = ["src"]
|
|
49
49
|
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
"sflkit.runners" = ["resources/*.java"]
|
|
52
|
+
|
|
50
53
|
[project.optional-dependencies]
|
|
51
54
|
test = [
|
|
52
55
|
"pytest>=8.3.4",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
4
|
+
from operator import itemgetter
|
|
4
5
|
from typing import List, Callable, Set, Dict, Optional, Any, Type
|
|
5
6
|
|
|
6
7
|
from sflkit.analysis.analysis_type import AnalysisType, AnalysisObject
|
|
@@ -25,6 +26,60 @@ from sflkit.events.event_file import EventFile
|
|
|
25
26
|
from sflkit.model.model import Model, MetaModel
|
|
26
27
|
from sflkit.model.parallel import ParallelModel
|
|
27
28
|
|
|
29
|
+
_key_of = itemgetter(0)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def analyze_files(
|
|
33
|
+
factory: AnalysisFactory, event_files: List[EventFile]
|
|
34
|
+
) -> Set[AnalysisObject]:
|
|
35
|
+
"""
|
|
36
|
+
Analyze a share of the runs, start to finish, in one process.
|
|
37
|
+
|
|
38
|
+
Defined at module level so it can be shipped to a worker process, and kept
|
|
39
|
+
free of shared state so that workers never have to talk to each other: the
|
|
40
|
+
runs are partitioned, so the per-run state each worker accumulates is
|
|
41
|
+
disjoint from every other worker's and merges by simple union.
|
|
42
|
+
|
|
43
|
+
Suspiciousness is deliberately *not* computed here. It is a property of the
|
|
44
|
+
whole suite, so it can only be derived once every worker's share has been
|
|
45
|
+
merged.
|
|
46
|
+
|
|
47
|
+
:param factory: A fresh analysis factory.
|
|
48
|
+
:param event_files: The runs this worker is responsible for.
|
|
49
|
+
:returns: The analysis objects it built.
|
|
50
|
+
"""
|
|
51
|
+
model = Model(factory)
|
|
52
|
+
for event_file in event_files:
|
|
53
|
+
model.prepare(event_file)
|
|
54
|
+
with event_file:
|
|
55
|
+
for event in event_file.load():
|
|
56
|
+
event.handle(model, event_file)
|
|
57
|
+
model.follow_up(event_file)
|
|
58
|
+
return model.get_analysis()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def merge_analysis(target: AnalysisObject, source: AnalysisObject) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Fold a worker's view of one analysis object into the parent's.
|
|
64
|
+
|
|
65
|
+
Everything an object accumulates while events are handled is keyed by run,
|
|
66
|
+
and workers hold disjoint runs, so merging is a dictionary update. Which
|
|
67
|
+
attributes those are is discovered rather than hard-coded, so a subclass
|
|
68
|
+
that adds per-run state of its own is merged too instead of silently
|
|
69
|
+
losing it.
|
|
70
|
+
|
|
71
|
+
:param target: The object the parent keeps.
|
|
72
|
+
:param source: The equal object a worker produced.
|
|
73
|
+
"""
|
|
74
|
+
for name, value in vars(source).items():
|
|
75
|
+
if not isinstance(value, dict) or not value:
|
|
76
|
+
continue
|
|
77
|
+
if not all(isinstance(key, EventFile) for key in value):
|
|
78
|
+
continue
|
|
79
|
+
current = getattr(target, name, None)
|
|
80
|
+
if isinstance(current, dict):
|
|
81
|
+
current.update(value)
|
|
82
|
+
|
|
28
83
|
|
|
29
84
|
class AnalysisEncoder(json.JSONEncoder):
|
|
30
85
|
def default(self, o: Any) -> Any:
|
|
@@ -44,6 +99,7 @@ class Analyzer:
|
|
|
44
99
|
model_class: Type[Model] = None,
|
|
45
100
|
parallel: bool = False,
|
|
46
101
|
workers: int = 4,
|
|
102
|
+
processes: int = 1,
|
|
47
103
|
):
|
|
48
104
|
if (
|
|
49
105
|
relevant_event_files is None
|
|
@@ -67,6 +123,10 @@ class Analyzer:
|
|
|
67
123
|
model_class = Model
|
|
68
124
|
self.model = model_class(factory)
|
|
69
125
|
self.workers = workers
|
|
126
|
+
#: Worker processes to spread the runs over. Threads cannot help here:
|
|
127
|
+
#: analysis is Python-level work and the GIL serializes it, while runs
|
|
128
|
+
#: are independent until suspiciousness is computed.
|
|
129
|
+
self.processes = processes
|
|
70
130
|
self.paths: Dict[int, os.PathLike] = dict()
|
|
71
131
|
self.max_suspiciousness = 0
|
|
72
132
|
self.min_suspiciousness = 0
|
|
@@ -87,9 +147,49 @@ class Analyzer:
|
|
|
87
147
|
raise NotImplementedError("Not implemented for meta/loaded analyzer")
|
|
88
148
|
self.model.finalize(self.irrelevant_event_files, self.relevant_event_files)
|
|
89
149
|
|
|
150
|
+
def _analyze_with_processes(self, event_files: List[EventFile]):
|
|
151
|
+
"""
|
|
152
|
+
Analyze *event_files* across worker processes and merge the results.
|
|
153
|
+
|
|
154
|
+
:param event_files: Every run to analyze.
|
|
155
|
+
|
|
156
|
+
Uses :class:`~concurrent.futures.ProcessPoolExecutor`, so on platforms
|
|
157
|
+
that start workers with ``spawn`` (macOS, Windows) the calling program
|
|
158
|
+
must guard its entry point with ``if __name__ == "__main__":``, as with
|
|
159
|
+
any use of multiprocessing.
|
|
160
|
+
"""
|
|
161
|
+
chunks: List[List[EventFile]] = [[] for _ in range(self.processes)]
|
|
162
|
+
# Round-robin rather than contiguous: runs differ wildly in length, and
|
|
163
|
+
# dealing them out keeps a worker from drawing all the long ones.
|
|
164
|
+
for index, event_file in enumerate(event_files):
|
|
165
|
+
chunks[index % self.processes].append(event_file)
|
|
166
|
+
chunks = [chunk for chunk in chunks if chunk]
|
|
167
|
+
merged: Dict[AnalysisObject, AnalysisObject] = dict()
|
|
168
|
+
with ProcessPoolExecutor(max_workers=len(chunks)) as executor:
|
|
169
|
+
futures = [
|
|
170
|
+
executor.submit(analyze_files, self.model.factory, chunk)
|
|
171
|
+
for chunk in chunks
|
|
172
|
+
]
|
|
173
|
+
for future in futures:
|
|
174
|
+
for analysis in future.result():
|
|
175
|
+
known = merged.get(analysis)
|
|
176
|
+
if known is None:
|
|
177
|
+
merged[analysis] = analysis
|
|
178
|
+
else:
|
|
179
|
+
merge_analysis(known, analysis)
|
|
180
|
+
# The factory owns the objects the rest of the analyzer reads back.
|
|
181
|
+
self.model.factory.adopt(merged.values())
|
|
182
|
+
|
|
90
183
|
def analyze(self):
|
|
91
184
|
if self.meta:
|
|
92
185
|
raise NotImplementedError("Not implemented for meta/loaded analyzer")
|
|
186
|
+
if self.processes > 1:
|
|
187
|
+
event_files = self.relevant_event_files + self.irrelevant_event_files
|
|
188
|
+
for event_file in event_files:
|
|
189
|
+
self.paths[event_file.run_id] = event_file.path
|
|
190
|
+
self._analyze_with_processes(event_files)
|
|
191
|
+
self._finalize()
|
|
192
|
+
return
|
|
93
193
|
if self.workers > 1:
|
|
94
194
|
event_files = self.relevant_event_files + self.irrelevant_event_files
|
|
95
195
|
for event_file in event_files:
|
|
@@ -155,11 +255,15 @@ class Analyzer:
|
|
|
155
255
|
),
|
|
156
256
|
analysis,
|
|
157
257
|
):
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
258
|
+
value = suggestion.suspiciousness
|
|
259
|
+
suspiciousness.append(value)
|
|
260
|
+
lines = suggestions.get(value)
|
|
261
|
+
if lines is None:
|
|
262
|
+
suggestions[value] = set(suggestion.lines)
|
|
161
263
|
else:
|
|
162
|
-
|
|
264
|
+
# Union with a set, not update with the list: the two insert in a
|
|
265
|
+
# different order, and that decides the order of Suggestion.lines.
|
|
266
|
+
lines |= set(suggestion.lines)
|
|
163
267
|
|
|
164
268
|
if suspiciousness:
|
|
165
269
|
self.max_suspiciousness = max(suspiciousness)
|
|
@@ -169,13 +273,12 @@ class Analyzer:
|
|
|
169
273
|
len(suspiciousness) // 2
|
|
170
274
|
]
|
|
171
275
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)[:]
|
|
276
|
+
# Sorting on the key skips Suggestion's rich comparisons; the order is
|
|
277
|
+
# the same, because that is the only thing a suggestion compares.
|
|
278
|
+
return [
|
|
279
|
+
Suggestion(list(lines), value)
|
|
280
|
+
for value, lines in sorted(suggestions.items(), key=_key_of, reverse=True)
|
|
281
|
+
]
|
|
179
282
|
|
|
180
283
|
def get_coverage_per_run(
|
|
181
284
|
self, type_: AnalysisType = None
|