sflkit 0.2.21__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.
Files changed (64) hide show
  1. {sflkit-0.2.21/src/sflkit.egg-info → sflkit-0.2.22}/PKG-INFO +2 -2
  2. {sflkit-0.2.21 → sflkit-0.2.22}/pyproject.toml +2 -2
  3. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/analyzer.py +20 -2
  4. sflkit-0.2.22/src/sflkit/evaluation.py +116 -0
  5. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/python/factory.py +13 -1
  6. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/runners/run.py +104 -22
  7. {sflkit-0.2.21 → sflkit-0.2.22/src/sflkit.egg-info}/PKG-INFO +2 -2
  8. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit.egg-info/SOURCES.txt +2 -0
  9. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit.egg-info/requires.txt +1 -1
  10. sflkit-0.2.22/tests/test_evaluation.py +166 -0
  11. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_runner.py +65 -0
  12. {sflkit-0.2.21 → sflkit-0.2.22}/LICENSE +0 -0
  13. {sflkit-0.2.21 → sflkit-0.2.22}/README.md +0 -0
  14. {sflkit-0.2.21 → sflkit-0.2.22}/setup.cfg +0 -0
  15. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/__init__.py +0 -0
  16. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/__init__.py +0 -0
  17. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/analysis_type.py +0 -0
  18. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/factory.py +0 -0
  19. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/mapping.py +0 -0
  20. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/predicate.py +0 -0
  21. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/similarity.py +0 -0
  22. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/spectra.py +0 -0
  23. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/analysis/suggestion.py +0 -0
  24. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/cli.py +0 -0
  25. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/color.py +0 -0
  26. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/config.py +0 -0
  27. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/instrumentation/__init__.py +0 -0
  28. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/instrumentation/dir_instrumentation.py +0 -0
  29. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/instrumentation/file_instrumentation.py +0 -0
  30. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/__init__.py +0 -0
  31. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/extract.py +0 -0
  32. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/finder.py +0 -0
  33. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/language.py +0 -0
  34. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/meta.py +0 -0
  35. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/python/__init__.py +0 -0
  36. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/python/extract.py +0 -0
  37. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/python/finder.py +0 -0
  38. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/python/visitor.py +0 -0
  39. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/language/visitor.py +0 -0
  40. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/logger.py +0 -0
  41. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/mapping.py +0 -0
  42. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/model/__init__.py +0 -0
  43. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/model/event_file.py +0 -0
  44. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/model/model.py +0 -0
  45. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/model/scope.py +0 -0
  46. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit/runners/__init__.py +0 -0
  47. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit.egg-info/dependency_links.txt +0 -0
  48. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit.egg-info/entry_points.txt +0 -0
  49. {sflkit-0.2.21 → sflkit-0.2.22}/src/sflkit.egg-info/top_level.txt +0 -0
  50. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_analysis_objects.py +0 -0
  51. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_cli.py +0 -0
  52. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_color.py +0 -0
  53. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_config.py +0 -0
  54. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_coverage.py +0 -0
  55. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_events.py +0 -0
  56. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_execution.py +0 -0
  57. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_instrumentation.py +0 -0
  58. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_language.py +0 -0
  59. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_predicates.py +0 -0
  60. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_runner_pytest_versions.py +0 -0
  61. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_scope.py +0 -0
  62. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_spectra.py +0 -0
  63. {sflkit-0.2.21 → sflkit-0.2.22}/tests/test_suggestions.py +0 -0
  64. {sflkit-0.2.21 → 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.21
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.3
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.21"
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.3",
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), objects
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=Name(id="TypeError"),
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(rf"<{obj} ['\"]?(?P<name>[^>'\"]*)['\"]?>")
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
- obj = structure(match.group("name"))
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)
@@ -246,10 +267,10 @@ class PytestRunner(Runner):
246
267
  self.set_python_path = set_python_path
247
268
 
248
269
  @staticmethod
249
- def _common_base(directory: Path, tests: List[str]) -> Path:
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
- def _normalize_paths(
265
- self,
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
- base = self._common_base(directory, tests)
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
- path, test = r.split("::", 1)
279
- result.append(
280
- str((base / path).relative_to(directory)) + "::" + test
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._normalize_paths(tests, directory, root_dir)
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.21
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.3
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
@@ -1,4 +1,4 @@
1
- sflkitlib>=0.0.3
1
+ sflkitlib>=0.0.5
2
2
  numpy>=1.23.5
3
3
  matplotlib>=3.7.2
4
4
  sortedcollections>=2.1.0
@@ -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