sflkit 0.2.20__tar.gz → 0.2.22__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.2.20/src/sflkit.egg-info → sflkit-0.2.22}/PKG-INFO +2 -2
- {sflkit-0.2.20 → sflkit-0.2.22}/pyproject.toml +2 -2
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/analyzer.py +20 -2
- sflkit-0.2.22/src/sflkit/evaluation.py +116 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/python/factory.py +13 -1
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/runners/run.py +105 -23
- {sflkit-0.2.20 → sflkit-0.2.22/src/sflkit.egg-info}/PKG-INFO +2 -2
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit.egg-info/SOURCES.txt +2 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit.egg-info/requires.txt +1 -1
- sflkit-0.2.22/tests/test_evaluation.py +166 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_runner.py +65 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/LICENSE +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/README.md +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/setup.cfg +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/__init__.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/__init__.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/analysis_type.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/factory.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/mapping.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/predicate.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/similarity.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/spectra.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/analysis/suggestion.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/cli.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/color.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/config.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/instrumentation/__init__.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/instrumentation/dir_instrumentation.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/instrumentation/file_instrumentation.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/__init__.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/extract.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/finder.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/language.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/meta.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/python/__init__.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/python/extract.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/python/finder.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/python/visitor.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/language/visitor.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/logger.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/mapping.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/model/__init__.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/model/event_file.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/model/model.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/model/scope.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit/runners/__init__.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit.egg-info/dependency_links.txt +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit.egg-info/entry_points.txt +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/src/sflkit.egg-info/top_level.txt +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_analysis_objects.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_cli.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_color.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_config.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_coverage.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_events.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_execution.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_instrumentation.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_language.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_predicates.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_runner_pytest_versions.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_scope.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_spectra.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_suggestions.py +0 -0
- {sflkit-0.2.20 → sflkit-0.2.22}/tests/test_visitors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sflkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.22
|
|
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/uds-se/sflkit/
|
|
@@ -13,7 +13,7 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: sflkitlib>=0.0.
|
|
16
|
+
Requires-Dist: sflkitlib>=0.0.5
|
|
17
17
|
Requires-Dist: numpy>=1.23.5
|
|
18
18
|
Requires-Dist: matplotlib>=3.7.2
|
|
19
19
|
Requires-Dist: sortedcollections>=2.1.0
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "sflkit"
|
|
10
|
-
version = "0.2.
|
|
10
|
+
version = "0.2.22"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Marius Smytzek", email = "marius.smytzek@cispa.de" },
|
|
13
13
|
]
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Testing"
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"sflkitlib>=0.0.
|
|
26
|
+
"sflkitlib>=0.0.5",
|
|
27
27
|
"numpy>=1.23.5",
|
|
28
28
|
"matplotlib>=3.7.2",
|
|
29
29
|
"sortedcollections>=2.1.0"
|
|
@@ -19,6 +19,9 @@ class Analyzer(object):
|
|
|
19
19
|
self.irrelevant_event_files = irrelevant_event_files
|
|
20
20
|
self.model = Model(factory)
|
|
21
21
|
self.paths: Dict[int, os.PathLike] = dict()
|
|
22
|
+
self.max_suspiciousness = 0
|
|
23
|
+
self.min_suspiciousness = 0
|
|
24
|
+
self.avg_suspiciousness = 0
|
|
22
25
|
|
|
23
26
|
def _analyze(self, event_file):
|
|
24
27
|
self.model.prepare(event_file)
|
|
@@ -48,20 +51,35 @@ class Analyzer(object):
|
|
|
48
51
|
|
|
49
52
|
def get_sorted_suggestions(
|
|
50
53
|
self, base_dir, metric: Callable = None, type_: AnalysisType = None
|
|
51
|
-
):
|
|
54
|
+
) -> List[Suggestion]:
|
|
52
55
|
if type_:
|
|
53
56
|
objects = self.get_analysis_by_type(type_)
|
|
54
57
|
else:
|
|
55
58
|
objects = self.get_analysis()
|
|
59
|
+
return self.get_sorted_suggestions_from_analysis(base_dir, objects, metric)
|
|
60
|
+
|
|
61
|
+
def get_sorted_suggestions_from_analysis(
|
|
62
|
+
self, base_dir, analysis: Set[AnalysisObject], metric: Callable = None
|
|
63
|
+
) -> List[Suggestion]:
|
|
56
64
|
suggestions = dict()
|
|
65
|
+
max_suspiciousness = float("-inf")
|
|
66
|
+
min_suspiciousness = float("inf")
|
|
67
|
+
avg_suspiciousness = 0
|
|
57
68
|
for suggestion in map(
|
|
58
|
-
lambda p: p.get_suggestion(metric=metric, base_dir=base_dir),
|
|
69
|
+
lambda p: p.get_suggestion(metric=metric, base_dir=base_dir), analysis
|
|
59
70
|
):
|
|
71
|
+
max_suspiciousness = max(max_suspiciousness, suggestion.suspiciousness)
|
|
72
|
+
min_suspiciousness = min(min_suspiciousness, suggestion.suspiciousness)
|
|
73
|
+
avg_suspiciousness += suggestion.suspiciousness
|
|
60
74
|
if suggestion.suspiciousness not in suggestions:
|
|
61
75
|
suggestions[suggestion.suspiciousness] = set(suggestion.lines)
|
|
62
76
|
else:
|
|
63
77
|
suggestions[suggestion.suspiciousness] |= set(suggestion.lines)
|
|
64
78
|
|
|
79
|
+
self.max_suspiciousness = max_suspiciousness
|
|
80
|
+
self.min_suspiciousness = min_suspiciousness
|
|
81
|
+
self.avg_suspiciousness = avg_suspiciousness / len(analysis)
|
|
82
|
+
|
|
65
83
|
return sorted(
|
|
66
84
|
[
|
|
67
85
|
Suggestion(list(lines), suspiciousness)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import random
|
|
3
|
+
from typing import List, Dict, Callable, Set, Optional
|
|
4
|
+
|
|
5
|
+
from sflkit.analysis.suggestion import Suggestion, Location
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Average:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.number_of_locations = 0
|
|
11
|
+
|
|
12
|
+
def average(self, suspiciousness: float, current_suspiciousness: float):
|
|
13
|
+
current_suspiciousness *= self.number_of_locations
|
|
14
|
+
self.number_of_locations += 1
|
|
15
|
+
return (current_suspiciousness + suspiciousness) / self.number_of_locations
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Scenario(enum.Enum):
|
|
19
|
+
BEST_CASE = "best_case"
|
|
20
|
+
AVG_CASE = "avg_case"
|
|
21
|
+
WORST_CASE = "worst_case"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Rank:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
suggestions: List[Suggestion],
|
|
28
|
+
metric: Callable[[float, float], float] = max,
|
|
29
|
+
default_suspiciousness: float = float("-inf"),
|
|
30
|
+
):
|
|
31
|
+
self.suggestions = sorted(suggestions, reverse=True)
|
|
32
|
+
self.suspiciousness: Dict[Location, float] = dict()
|
|
33
|
+
self.ranks: Dict[float, List[Location]] = dict()
|
|
34
|
+
self.locations: Dict[Location, float] = dict()
|
|
35
|
+
current_rank = 1
|
|
36
|
+
for i, suggestion in enumerate(self.suggestions):
|
|
37
|
+
lines = suggestion.lines
|
|
38
|
+
if len(lines) == 0:
|
|
39
|
+
continue
|
|
40
|
+
elif len(lines) == 1:
|
|
41
|
+
rank = current_rank
|
|
42
|
+
current_rank += 1
|
|
43
|
+
else:
|
|
44
|
+
rank = (len(lines)) / 2 + (current_rank - 1)
|
|
45
|
+
current_rank += len(lines)
|
|
46
|
+
self.ranks[rank] = lines
|
|
47
|
+
for line in lines:
|
|
48
|
+
self.suspiciousness[line] = metric(
|
|
49
|
+
suggestion.suspiciousness,
|
|
50
|
+
self.locations.get(line, default_suspiciousness),
|
|
51
|
+
)
|
|
52
|
+
self.locations[line] = rank
|
|
53
|
+
self.number_of_locations = len(self.locations)
|
|
54
|
+
|
|
55
|
+
def top_n(
|
|
56
|
+
self,
|
|
57
|
+
faulty: Set[Location],
|
|
58
|
+
n: int,
|
|
59
|
+
scenario: Optional[Scenario] = None,
|
|
60
|
+
repeat: int = 1000,
|
|
61
|
+
) -> float:
|
|
62
|
+
top_n_locations = list()
|
|
63
|
+
for suggestion in self.suggestions:
|
|
64
|
+
if len(top_n_locations) >= n:
|
|
65
|
+
break
|
|
66
|
+
for line in suggestion.lines:
|
|
67
|
+
if line not in top_n_locations:
|
|
68
|
+
top_n_locations.append(line)
|
|
69
|
+
if len(top_n_locations) <= n:
|
|
70
|
+
return self._top_n(faulty, top_n_locations, scenario)
|
|
71
|
+
else:
|
|
72
|
+
sum_ = 0
|
|
73
|
+
for _ in range(repeat):
|
|
74
|
+
sum_ += self._top_n(
|
|
75
|
+
faulty, random.sample(top_n_locations, k=n), scenario
|
|
76
|
+
)
|
|
77
|
+
return sum_ / repeat
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _top_n(
|
|
81
|
+
faulty: Set[Location],
|
|
82
|
+
top_n_locations: List[Location],
|
|
83
|
+
scenario: Optional[Scenario] = None,
|
|
84
|
+
) -> float:
|
|
85
|
+
found = len(faulty.intersection(top_n_locations))
|
|
86
|
+
if scenario == Scenario.BEST_CASE:
|
|
87
|
+
return 1 if found > 0 else 0
|
|
88
|
+
elif scenario == Scenario.WORST_CASE:
|
|
89
|
+
return found / len(faulty)
|
|
90
|
+
elif scenario == Scenario.AVG_CASE:
|
|
91
|
+
return min(found / (len(faulty) / 2), 1)
|
|
92
|
+
else:
|
|
93
|
+
return found / len(top_n_locations)
|
|
94
|
+
|
|
95
|
+
def get_rank(
|
|
96
|
+
self, faulty: Set[Location], scenario: Optional[Scenario] = None
|
|
97
|
+
) -> float:
|
|
98
|
+
if scenario == Scenario.BEST_CASE:
|
|
99
|
+
rank = min(self.locations[location] for location in faulty)
|
|
100
|
+
elif scenario == Scenario.WORST_CASE:
|
|
101
|
+
rank = max(self.locations[location] for location in faulty)
|
|
102
|
+
elif scenario == Scenario.AVG_CASE:
|
|
103
|
+
rank = sorted([self.locations[location] for location in faulty])[
|
|
104
|
+
max(len(faulty) // 2 - 1, 0)
|
|
105
|
+
]
|
|
106
|
+
else:
|
|
107
|
+
rank = sum(self.locations[location] for location in faulty) / len(faulty)
|
|
108
|
+
return rank
|
|
109
|
+
|
|
110
|
+
def exam(self, faulty: Set[Location], scenario: Optional[Scenario] = None) -> float:
|
|
111
|
+
return self.get_rank(faulty, scenario) / self.number_of_locations
|
|
112
|
+
|
|
113
|
+
def wasted_effort(
|
|
114
|
+
self, faulty: Set[Location], scenario: Optional[Scenario] = None
|
|
115
|
+
) -> float:
|
|
116
|
+
return self.get_rank(faulty, scenario)
|
|
@@ -853,7 +853,19 @@ class LenEventFactory(DefEventFactory):
|
|
|
853
853
|
body=[call],
|
|
854
854
|
handlers=[
|
|
855
855
|
ExceptHandler(
|
|
856
|
-
type=
|
|
856
|
+
type=Tuple(
|
|
857
|
+
elts=[
|
|
858
|
+
Name(
|
|
859
|
+
id="AttributeError",
|
|
860
|
+
),
|
|
861
|
+
Name(
|
|
862
|
+
id="TypeError",
|
|
863
|
+
),
|
|
864
|
+
Name(
|
|
865
|
+
id="NameError",
|
|
866
|
+
),
|
|
867
|
+
],
|
|
868
|
+
),
|
|
857
869
|
name=None,
|
|
858
870
|
body=[Pass()],
|
|
859
871
|
)
|
|
@@ -7,7 +7,7 @@ import shutil
|
|
|
7
7
|
import string
|
|
8
8
|
import subprocess
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import List, Dict, Optional, Tuple
|
|
10
|
+
from typing import List, Dict, Optional, Tuple, Set
|
|
11
11
|
|
|
12
12
|
from sflkit.logger import LOGGER
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ PYTEST_RESULT_PATTERN = re.compile(
|
|
|
17
17
|
rb"=( |\x1b\[\d+m)+(("
|
|
18
18
|
rb"((?P<f>\d+) failed)|"
|
|
19
19
|
rb"((?P<p>\d+) passed)|"
|
|
20
|
-
rb"(\d+ (skipped|warning(s?)|"
|
|
20
|
+
rb"(\d+ (skipped|warning(s?)|deselected|"
|
|
21
21
|
rb"(error(s?)))))"
|
|
22
22
|
rb"(( |\x1b\[\d+m)*,( |\x1b\[\d+m)+)?)+( |\x1b\[\d+m)+in( |\x1b\[\d+m)+"
|
|
23
23
|
)
|
|
@@ -157,7 +157,17 @@ class PytestStructure:
|
|
|
157
157
|
|
|
158
158
|
@staticmethod
|
|
159
159
|
def get_pattern(obj):
|
|
160
|
-
return re.compile(
|
|
160
|
+
return re.compile(
|
|
161
|
+
rf"<{obj} "
|
|
162
|
+
r"('(?P<name_single>([^']|\\')*)'"
|
|
163
|
+
r"|\"(?P<name_double>([^\"]|\\\")*)\""
|
|
164
|
+
r"|(?P<name>[^\"'].*))"
|
|
165
|
+
r">"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def clean_line(line: str) -> str:
|
|
170
|
+
return line.replace("\\\\", "\\")
|
|
161
171
|
|
|
162
172
|
@staticmethod
|
|
163
173
|
def parse_tests(output: str) -> List[str]:
|
|
@@ -193,7 +203,18 @@ class PytestStructure:
|
|
|
193
203
|
continue
|
|
194
204
|
match = pattern.search(line)
|
|
195
205
|
if match:
|
|
196
|
-
|
|
206
|
+
name = (
|
|
207
|
+
match.group("name_single")
|
|
208
|
+
or match.group("name_double")
|
|
209
|
+
or match.group("name")
|
|
210
|
+
)
|
|
211
|
+
if match.group("name_single"):
|
|
212
|
+
name = PytestStructure.clean_line(name)
|
|
213
|
+
name = name.replace("\\'", "'")
|
|
214
|
+
elif match.group("name_double"):
|
|
215
|
+
name = PytestStructure.clean_line(name)
|
|
216
|
+
name = name.replace('\\"', '"')
|
|
217
|
+
obj = structure(name)
|
|
197
218
|
obj.depth = level
|
|
198
219
|
if current is None:
|
|
199
220
|
trees.append(obj)
|
|
@@ -201,7 +222,7 @@ class PytestStructure:
|
|
|
201
222
|
obj.set_parent(current)
|
|
202
223
|
current.add_child(obj)
|
|
203
224
|
elif current.depth >= obj.depth:
|
|
204
|
-
while current.depth >= obj.depth
|
|
225
|
+
while current and current.depth >= obj.depth:
|
|
205
226
|
current = current.parent
|
|
206
227
|
if current is None:
|
|
207
228
|
trees.append(obj)
|
|
@@ -246,10 +267,10 @@ class PytestRunner(Runner):
|
|
|
246
267
|
self.set_python_path = set_python_path
|
|
247
268
|
|
|
248
269
|
@staticmethod
|
|
249
|
-
def
|
|
270
|
+
def common_base(directory: Path, tests: List[str]) -> Path:
|
|
250
271
|
parts = directory.parts
|
|
251
272
|
common_bases = {Path(*parts[:i]) for i in range(1, len(parts) + 1)}
|
|
252
|
-
leaves_paths = {Path(r.split("::")[0]) for r in tests}
|
|
273
|
+
leaves_paths = {Path(r.split("::", 1)[0] if "::" in r else r) for r in tests}
|
|
253
274
|
common_bases = set(
|
|
254
275
|
filter(
|
|
255
276
|
lambda p: all(map(lambda r: Path(p, *r.parts).exists(), leaves_paths)),
|
|
@@ -261,24 +282,82 @@ class PytestRunner(Runner):
|
|
|
261
282
|
else:
|
|
262
283
|
return None
|
|
263
284
|
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
@staticmethod
|
|
286
|
+
def common_path(files: List[str]) -> Optional[Path]:
|
|
287
|
+
paths = []
|
|
288
|
+
for f in files:
|
|
289
|
+
if "::" in f:
|
|
290
|
+
paths.append(f.split("::")[0])
|
|
291
|
+
else:
|
|
292
|
+
paths.append(f)
|
|
293
|
+
common_path = os.path.commonpath(paths)
|
|
294
|
+
if common_path:
|
|
295
|
+
return Path(common_path)
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def get_files(files: List[os.PathLike]) -> Set[str]:
|
|
300
|
+
paths = set()
|
|
301
|
+
for f in files:
|
|
302
|
+
f = str(f)
|
|
303
|
+
if "::" in f:
|
|
304
|
+
paths.add(f.split("::", 1)[0])
|
|
305
|
+
else:
|
|
306
|
+
paths.add(f)
|
|
307
|
+
return paths
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def get_absolute_files(
|
|
311
|
+
files: Set[str], directory: Optional[Path] = None
|
|
312
|
+
) -> Set[Path]:
|
|
313
|
+
result = set()
|
|
314
|
+
for f in files:
|
|
315
|
+
f = Path(f)
|
|
316
|
+
if f.is_absolute() or directory is None:
|
|
317
|
+
result.add(f)
|
|
318
|
+
else:
|
|
319
|
+
result.add(directory / f)
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def normalize_paths(
|
|
266
324
|
tests: List[str],
|
|
325
|
+
files: Set[Path] = None,
|
|
267
326
|
directory: Optional[Path] = None,
|
|
268
327
|
root_dir: Optional[Path] = None,
|
|
269
328
|
):
|
|
270
329
|
result = tests
|
|
330
|
+
|
|
271
331
|
if directory:
|
|
272
|
-
|
|
273
|
-
if base is None and root_dir:
|
|
274
|
-
base = self._common_base(root_dir, tests)
|
|
275
|
-
if base is not None:
|
|
332
|
+
if files:
|
|
276
333
|
result = []
|
|
277
334
|
for r in tests:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
335
|
+
for f in files:
|
|
336
|
+
base = PytestRunner.common_base(f, [r])
|
|
337
|
+
if base is not None:
|
|
338
|
+
path, test = r.split("::", 1)
|
|
339
|
+
result.append(
|
|
340
|
+
str((base / path).relative_to(directory)) + "::" + test
|
|
341
|
+
)
|
|
342
|
+
break
|
|
343
|
+
else:
|
|
344
|
+
base = PytestRunner.common_base(directory, tests)
|
|
345
|
+
if base is None and root_dir:
|
|
346
|
+
base = PytestRunner.common_base(root_dir, tests)
|
|
347
|
+
if base is None and root_dir is not None:
|
|
348
|
+
result = []
|
|
349
|
+
for r in tests:
|
|
350
|
+
path, test = r.split("::", 1)
|
|
351
|
+
result.append(
|
|
352
|
+
str((root_dir / path).relative_to(directory)) + "::" + test
|
|
353
|
+
)
|
|
354
|
+
elif base is not None:
|
|
355
|
+
result = []
|
|
356
|
+
for r in tests:
|
|
357
|
+
path, test = r.split("::", 1)
|
|
358
|
+
result.append(
|
|
359
|
+
str((base / path).relative_to(directory)) + "::" + test
|
|
360
|
+
)
|
|
282
361
|
return result
|
|
283
362
|
|
|
284
363
|
def get_tests(
|
|
@@ -294,17 +373,20 @@ class PytestRunner(Runner):
|
|
|
294
373
|
if k:
|
|
295
374
|
c.append("-k")
|
|
296
375
|
c.append(k)
|
|
297
|
-
if files:
|
|
298
|
-
if isinstance(files, (str, os.PathLike)):
|
|
299
|
-
c.append(str(files))
|
|
300
|
-
else:
|
|
301
|
-
c += [str(f) for f in files]
|
|
302
376
|
if base:
|
|
303
377
|
if not files:
|
|
304
378
|
c.append(str(base))
|
|
305
379
|
root_dir = directory / base
|
|
306
380
|
else:
|
|
307
381
|
root_dir = directory
|
|
382
|
+
file_bases = None
|
|
383
|
+
if files:
|
|
384
|
+
if isinstance(files, (str, os.PathLike)):
|
|
385
|
+
str_files = [str(files)]
|
|
386
|
+
else:
|
|
387
|
+
str_files = [str(f) for f in files]
|
|
388
|
+
file_bases = self.get_absolute_files(self.get_files(str_files), directory)
|
|
389
|
+
c += str_files
|
|
308
390
|
process = subprocess.run(
|
|
309
391
|
[
|
|
310
392
|
"python3",
|
|
@@ -320,7 +402,7 @@ class PytestRunner(Runner):
|
|
|
320
402
|
)
|
|
321
403
|
LOGGER.info(f"pytest collection finished with {process.returncode}")
|
|
322
404
|
tests = PytestStructure.parse_tests(process.stdout.decode("utf8"))
|
|
323
|
-
return self.
|
|
405
|
+
return self.normalize_paths(tests, file_bases, directory, root_dir)
|
|
324
406
|
|
|
325
407
|
@staticmethod
|
|
326
408
|
def __get_pytest_result__(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sflkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.22
|
|
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/uds-se/sflkit/
|
|
@@ -13,7 +13,7 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: sflkitlib>=0.0.
|
|
16
|
+
Requires-Dist: sflkitlib>=0.0.5
|
|
17
17
|
Requires-Dist: numpy>=1.23.5
|
|
18
18
|
Requires-Dist: matplotlib>=3.7.2
|
|
19
19
|
Requires-Dist: sortedcollections>=2.1.0
|
|
@@ -5,6 +5,7 @@ src/sflkit/__init__.py
|
|
|
5
5
|
src/sflkit/cli.py
|
|
6
6
|
src/sflkit/color.py
|
|
7
7
|
src/sflkit/config.py
|
|
8
|
+
src/sflkit/evaluation.py
|
|
8
9
|
src/sflkit/logger.py
|
|
9
10
|
src/sflkit/mapping.py
|
|
10
11
|
src/sflkit.egg-info/PKG-INFO
|
|
@@ -47,6 +48,7 @@ tests/test_cli.py
|
|
|
47
48
|
tests/test_color.py
|
|
48
49
|
tests/test_config.py
|
|
49
50
|
tests/test_coverage.py
|
|
51
|
+
tests/test_evaluation.py
|
|
50
52
|
tests/test_events.py
|
|
51
53
|
tests/test_execution.py
|
|
52
54
|
tests/test_instrumentation.py
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from sflkit.analysis.suggestion import Suggestion, Location
|
|
5
|
+
from sflkit.evaluation import Rank, Scenario
|
|
6
|
+
from utils import BaseTest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestEvaluation(BaseTest):
|
|
10
|
+
def setUp(self):
|
|
11
|
+
random.seed(0)
|
|
12
|
+
self.suggestions = [
|
|
13
|
+
Suggestion([Location("a.py", 1)], 0.5),
|
|
14
|
+
Suggestion([Location("a.py", 2)], 0.3),
|
|
15
|
+
Suggestion([Location("a.py", 3)], 0.7),
|
|
16
|
+
Suggestion([Location("a.py", 4)], 0.1),
|
|
17
|
+
Suggestion([Location("a.py", 5)], 0.9),
|
|
18
|
+
Suggestion([Location("a.py", 6)], 0.2),
|
|
19
|
+
Suggestion([Location("a.py", 7)], 0.8),
|
|
20
|
+
Suggestion([Location("a.py", 8)], 0.4),
|
|
21
|
+
Suggestion([Location("a.py", 9)], 0.6),
|
|
22
|
+
Suggestion([Location("a.py", 10)], 0.0),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def get_rank(self, multi: bool = False):
|
|
26
|
+
suggestions = (
|
|
27
|
+
self.suggestions
|
|
28
|
+
if not multi
|
|
29
|
+
else self.suggestions
|
|
30
|
+
+ [
|
|
31
|
+
Suggestion(
|
|
32
|
+
[Location("a.py", 11), Location("a.py", 12), Location("a.py", 13)],
|
|
33
|
+
0.55,
|
|
34
|
+
)
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
return (
|
|
38
|
+
Rank(suggestions),
|
|
39
|
+
{
|
|
40
|
+
Location("a.py", 2),
|
|
41
|
+
Location("a.py", 6),
|
|
42
|
+
Location("a.py", 7),
|
|
43
|
+
Location("a.py", 8),
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def get_top_n(
|
|
48
|
+
self, n: int, scenario: Optional[Scenario] = None, multi: bool = False
|
|
49
|
+
):
|
|
50
|
+
rank, locations = self.get_rank(multi=multi)
|
|
51
|
+
return rank.top_n(
|
|
52
|
+
locations,
|
|
53
|
+
n,
|
|
54
|
+
scenario=scenario,
|
|
55
|
+
repeat=10000,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def get_exam(self, scenario: Optional[Scenario] = None):
|
|
59
|
+
rank, locations = self.get_rank(multi=True)
|
|
60
|
+
return rank.exam(
|
|
61
|
+
locations,
|
|
62
|
+
scenario=scenario,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def get_wasted_effort(self, scenario: Optional[Scenario] = None):
|
|
66
|
+
rank, locations = self.get_rank(multi=True)
|
|
67
|
+
return rank.wasted_effort(
|
|
68
|
+
locations,
|
|
69
|
+
scenario=scenario,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def test_top_1(self):
|
|
73
|
+
top_1 = self.get_top_n(1)
|
|
74
|
+
self.assertAlmostEqual(0, top_1, delta=self.DELTA)
|
|
75
|
+
|
|
76
|
+
def test_top_5(self):
|
|
77
|
+
top_5 = self.get_top_n(5)
|
|
78
|
+
self.assertAlmostEqual(0.2, top_5, delta=self.DELTA)
|
|
79
|
+
|
|
80
|
+
def test_top_10(self):
|
|
81
|
+
top_10 = self.get_top_n(10)
|
|
82
|
+
self.assertAlmostEqual(0.4, top_10, delta=self.DELTA)
|
|
83
|
+
|
|
84
|
+
def test_top_5_multi(self):
|
|
85
|
+
top_5_multi = self.get_top_n(5, multi=True)
|
|
86
|
+
self.assertAlmostEqual(0.14285, top_5_multi, delta=0.05)
|
|
87
|
+
|
|
88
|
+
def test_top_1_avg(self):
|
|
89
|
+
top_1_avg = self.get_top_n(1, scenario=Scenario.AVG_CASE)
|
|
90
|
+
self.assertAlmostEqual(0, top_1_avg, delta=self.DELTA)
|
|
91
|
+
|
|
92
|
+
def test_top_5_avg(self):
|
|
93
|
+
top_5_avg = self.get_top_n(5, scenario=Scenario.AVG_CASE)
|
|
94
|
+
self.assertAlmostEqual(0.5, top_5_avg, delta=self.DELTA)
|
|
95
|
+
|
|
96
|
+
def test_top_10_avg(self):
|
|
97
|
+
top_10_avg = self.get_top_n(10, scenario=Scenario.AVG_CASE)
|
|
98
|
+
self.assertAlmostEqual(1, top_10_avg, delta=self.DELTA)
|
|
99
|
+
|
|
100
|
+
def test_top_5_avg_multi(self):
|
|
101
|
+
top_5_avg_multi = self.get_top_n(5, scenario=Scenario.AVG_CASE, multi=True)
|
|
102
|
+
self.assertAlmostEqual(0.35714, top_5_avg_multi, delta=0.005)
|
|
103
|
+
|
|
104
|
+
def test_top_1_best(self):
|
|
105
|
+
top_1_best = self.get_top_n(1, scenario=Scenario.BEST_CASE)
|
|
106
|
+
self.assertAlmostEqual(0, top_1_best, delta=self.DELTA)
|
|
107
|
+
|
|
108
|
+
def test_top_5_best(self):
|
|
109
|
+
top_5_best = self.get_top_n(5, scenario=Scenario.BEST_CASE)
|
|
110
|
+
self.assertAlmostEqual(1, top_5_best, delta=self.DELTA)
|
|
111
|
+
|
|
112
|
+
def test_top_10_best(self):
|
|
113
|
+
top_10_best = self.get_top_n(10, scenario=Scenario.BEST_CASE)
|
|
114
|
+
self.assertAlmostEqual(1, top_10_best, delta=self.DELTA)
|
|
115
|
+
|
|
116
|
+
def test_top_5_best_multi(self):
|
|
117
|
+
top_5_best_multi = self.get_top_n(5, scenario=Scenario.BEST_CASE, multi=True)
|
|
118
|
+
self.assertAlmostEqual(0.71429, top_5_best_multi, delta=0.005)
|
|
119
|
+
|
|
120
|
+
def test_top_1_worst(self):
|
|
121
|
+
top_1_worst = self.get_top_n(1, scenario=Scenario.WORST_CASE)
|
|
122
|
+
self.assertAlmostEqual(0, top_1_worst, delta=self.DELTA)
|
|
123
|
+
|
|
124
|
+
def test_top_5_worst(self):
|
|
125
|
+
top_5_worst = self.get_top_n(5, scenario=Scenario.WORST_CASE)
|
|
126
|
+
self.assertAlmostEqual(0.25, top_5_worst, delta=self.DELTA)
|
|
127
|
+
|
|
128
|
+
def test_top_10_worst(self):
|
|
129
|
+
top_10_worst = self.get_top_n(10, scenario=Scenario.WORST_CASE)
|
|
130
|
+
self.assertAlmostEqual(1, top_10_worst, delta=self.DELTA)
|
|
131
|
+
|
|
132
|
+
def test_top_5_worst_multi(self):
|
|
133
|
+
top_5_worst_multi = self.get_top_n(5, scenario=Scenario.WORST_CASE, multi=True)
|
|
134
|
+
self.assertAlmostEqual(0.17857, top_5_worst_multi, delta=0.005)
|
|
135
|
+
|
|
136
|
+
def test_exam_avg(self):
|
|
137
|
+
exam_avg = self.get_exam(scenario=Scenario.AVG_CASE)
|
|
138
|
+
self.assertAlmostEqual(9 / 13, exam_avg, delta=self.DELTA)
|
|
139
|
+
|
|
140
|
+
def test_exam_best(self):
|
|
141
|
+
exam_best = self.get_exam(scenario=Scenario.BEST_CASE)
|
|
142
|
+
self.assertAlmostEqual(2 / 13, exam_best, delta=self.DELTA)
|
|
143
|
+
|
|
144
|
+
def test_exam_worst(self):
|
|
145
|
+
exam_worst = self.get_exam(scenario=Scenario.WORST_CASE)
|
|
146
|
+
self.assertAlmostEqual(11 / 13, exam_worst, delta=self.DELTA)
|
|
147
|
+
|
|
148
|
+
def test_exam(self):
|
|
149
|
+
exam = self.get_exam()
|
|
150
|
+
self.assertAlmostEqual((2 + 9 + 10 + 11) / (13 * 4), exam, delta=self.DELTA)
|
|
151
|
+
|
|
152
|
+
def test_wasted_effort_avg(self):
|
|
153
|
+
wasted_effort_avg = self.get_wasted_effort(scenario=Scenario.AVG_CASE)
|
|
154
|
+
self.assertAlmostEqual(9, wasted_effort_avg, delta=self.DELTA)
|
|
155
|
+
|
|
156
|
+
def test_wasted_effort_best(self):
|
|
157
|
+
wasted_effort_best = self.get_wasted_effort(scenario=Scenario.BEST_CASE)
|
|
158
|
+
self.assertAlmostEqual(2, wasted_effort_best, delta=self.DELTA)
|
|
159
|
+
|
|
160
|
+
def test_wasted_effort_worst(self):
|
|
161
|
+
wasted_effort_worst = self.get_wasted_effort(scenario=Scenario.WORST_CASE)
|
|
162
|
+
self.assertAlmostEqual(11, wasted_effort_worst, delta=self.DELTA)
|
|
163
|
+
|
|
164
|
+
def test_wasted_effort(self):
|
|
165
|
+
wasted_effort = self.get_wasted_effort()
|
|
166
|
+
self.assertAlmostEqual((2 + 9 + 10 + 11) / 4, wasted_effort, delta=self.DELTA)
|
|
@@ -9,6 +9,7 @@ from sflkit.model import EventFile
|
|
|
9
9
|
from sflkit.runners.run import (
|
|
10
10
|
PytestRunner,
|
|
11
11
|
InputRunner,
|
|
12
|
+
PytestStructure,
|
|
12
13
|
)
|
|
13
14
|
from tests.utils import BaseTest
|
|
14
15
|
|
|
@@ -112,3 +113,67 @@ class RunnerTests(BaseTest):
|
|
|
112
113
|
self.assertEqual(1, suggestions[-1].suspiciousness)
|
|
113
114
|
self.assertEqual(1, len(suggestions[-1].lines))
|
|
114
115
|
self.assertEqual(Location("main.py", 10), suggestions[-1].lines[0])
|
|
116
|
+
|
|
117
|
+
def test_parse_and_paths(self):
|
|
118
|
+
collect = (
|
|
119
|
+
"\n"
|
|
120
|
+
"<Package b>\n"
|
|
121
|
+
" <Module test_b.py>\n"
|
|
122
|
+
" <Function test_d>\n"
|
|
123
|
+
"<Package tests>\n"
|
|
124
|
+
" <Module test_a.py>\n"
|
|
125
|
+
" <Function test_a>\n"
|
|
126
|
+
" <Function test_b>\n"
|
|
127
|
+
" <Function test_c>\n"
|
|
128
|
+
)
|
|
129
|
+
tests = PytestStructure.parse_tests(collect)
|
|
130
|
+
self.assertEqual(4, len(tests))
|
|
131
|
+
self.assertIn(os.path.join("b", "test_b.py::test_d"), tests)
|
|
132
|
+
self.assertIn(os.path.join("tests", "test_a.py::test_a"), tests)
|
|
133
|
+
self.assertIn(os.path.join("tests", "test_a.py::test_b"), tests)
|
|
134
|
+
self.assertIn(os.path.join("tests", "test_a.py::test_c"), tests)
|
|
135
|
+
files = PytestRunner.get_files(
|
|
136
|
+
[
|
|
137
|
+
Path("structure", "tests", "b", "test_b.py"),
|
|
138
|
+
Path("structure", "tests", "test_a.py::test_a"),
|
|
139
|
+
Path("structure", "tests", "test_a.py::test_b"),
|
|
140
|
+
Path("structure", "tests", "test_a.py::test_c"),
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
self.assertEqual(2, len(files))
|
|
144
|
+
self.assertIn(os.path.join("structure", "tests", "b", "test_b.py"), files)
|
|
145
|
+
self.assertIn(os.path.join("structure", "tests", "test_a.py"), files)
|
|
146
|
+
directory = Path(BaseTest.TEST_RESOURCES, "structure")
|
|
147
|
+
files = PytestRunner.get_absolute_files(
|
|
148
|
+
files,
|
|
149
|
+
directory,
|
|
150
|
+
)
|
|
151
|
+
self.assertEqual(2, len(files))
|
|
152
|
+
self.assertIn(
|
|
153
|
+
directory / "structure" / "tests" / "b" / "test_b.py",
|
|
154
|
+
files,
|
|
155
|
+
)
|
|
156
|
+
self.assertIn(directory / "structure" / "tests" / "test_a.py", files)
|
|
157
|
+
tests = PytestRunner.normalize_paths(
|
|
158
|
+
tests,
|
|
159
|
+
files=files,
|
|
160
|
+
directory=directory,
|
|
161
|
+
root_dir=directory / "structure" / "tests",
|
|
162
|
+
)
|
|
163
|
+
self.assertEqual(4, len(tests))
|
|
164
|
+
self.assertIn(
|
|
165
|
+
os.path.join("structure", "tests", "b", "test_b.py::test_d"),
|
|
166
|
+
tests,
|
|
167
|
+
)
|
|
168
|
+
self.assertIn(
|
|
169
|
+
os.path.join("structure", "tests", "test_a.py::test_a"),
|
|
170
|
+
tests,
|
|
171
|
+
)
|
|
172
|
+
self.assertIn(
|
|
173
|
+
os.path.join("structure", "tests", "test_a.py::test_b"),
|
|
174
|
+
tests,
|
|
175
|
+
)
|
|
176
|
+
self.assertIn(
|
|
177
|
+
os.path.join("structure", "tests", "test_a.py::test_c"),
|
|
178
|
+
tests,
|
|
179
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|