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.
Files changed (104) hide show
  1. {sflkit-0.5.6/src/sflkit.egg-info → sflkit-1.0.0}/PKG-INFO +13 -4
  2. {sflkit-0.5.6 → sflkit-1.0.0}/README.md +10 -1
  3. {sflkit-0.5.6 → sflkit-1.0.0}/pyproject.toml +5 -2
  4. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/__init__.py +1 -1
  5. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/analyzer.py +115 -12
  6. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/factory.py +324 -41
  7. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/predicate.py +45 -29
  8. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/spectra.py +21 -1
  9. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/suggestion.py +21 -2
  10. sflkit-1.0.0/src/sflkit/evaluation.py +201 -0
  11. sflkit-1.0.0/src/sflkit/events/event_file.py +121 -0
  12. sflkit-1.0.0/src/sflkit/features/handler.py +313 -0
  13. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/features/value.py +38 -1
  14. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/features/vector.py +22 -2
  15. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/java/extract.py +111 -36
  16. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/java/factory.py +363 -69
  17. sflkit-1.0.0/src/sflkit/language/java/finder.py +97 -0
  18. sflkit-1.0.0/src/sflkit/language/java/visitor.py +310 -0
  19. sflkit-1.0.0/src/sflkit/language/language.py +196 -0
  20. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/meta.py +13 -1
  21. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/factory.py +343 -79
  22. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/visitor.py +96 -2
  23. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/model/model.py +29 -3
  24. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/model/parallel.py +11 -6
  25. sflkit-1.0.0/src/sflkit/model/scope.py +155 -0
  26. sflkit-1.0.0/src/sflkit/online/__init__.py +57 -0
  27. sflkit-1.0.0/src/sflkit/online/listener.py +266 -0
  28. sflkit-1.0.0/src/sflkit/online/plugin.py +187 -0
  29. sflkit-1.0.0/src/sflkit/online/session.py +349 -0
  30. sflkit-1.0.0/src/sflkit/online/structure.py +210 -0
  31. sflkit-1.0.0/src/sflkit/online/tracer.py +1238 -0
  32. sflkit-1.0.0/src/sflkit/online/tree.py +242 -0
  33. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/runners/__init__.py +3 -0
  34. sflkit-1.0.0/src/sflkit/runners/defects4j.py +429 -0
  35. sflkit-1.0.0/src/sflkit/runners/resources/SflkitTestRunner.java +47 -0
  36. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/runners/run.py +180 -31
  37. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/weights/models.py +3 -1
  38. {sflkit-0.5.6 → sflkit-1.0.0/src/sflkit.egg-info}/PKG-INFO +13 -4
  39. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/SOURCES.txt +12 -0
  40. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/requires.txt +2 -2
  41. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_analyzer.py +45 -0
  42. sflkit-1.0.0/tests/test_defects4j.py +75 -0
  43. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_evaluation.py +107 -1
  44. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_events.py +13 -3
  45. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_features.py +164 -1
  46. sflkit-1.0.0/tests/test_instrumentation.py +514 -0
  47. sflkit-1.0.0/tests/test_java.py +673 -0
  48. sflkit-1.0.0/tests/test_online.py +741 -0
  49. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_runner.py +69 -1
  50. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_visitors.py +7 -0
  51. sflkit-0.5.6/src/sflkit/evaluation.py +0 -172
  52. sflkit-0.5.6/src/sflkit/events/event_file.py +0 -58
  53. sflkit-0.5.6/src/sflkit/features/handler.py +0 -182
  54. sflkit-0.5.6/src/sflkit/language/java/finder.py +0 -0
  55. sflkit-0.5.6/src/sflkit/language/java/visitor.py +0 -171
  56. sflkit-0.5.6/src/sflkit/language/language.py +0 -94
  57. sflkit-0.5.6/src/sflkit/model/scope.py +0 -82
  58. sflkit-0.5.6/tests/test_instrumentation.py +0 -263
  59. {sflkit-0.5.6 → sflkit-1.0.0}/LICENSE +0 -0
  60. {sflkit-0.5.6 → sflkit-1.0.0}/setup.cfg +0 -0
  61. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/__init__.py +0 -0
  62. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/analysis_type.py +0 -0
  63. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/mapping.py +0 -0
  64. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/analysis/similarity.py +0 -0
  65. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/cli.py +0 -0
  66. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/color.py +0 -0
  67. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/config.py +0 -0
  68. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/events/__init__.py +0 -0
  69. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/events/mapping.py +0 -0
  70. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/features/__init__.py +0 -0
  71. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/instrumentation/__init__.py +0 -0
  72. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/instrumentation/dir_instrumentation.py +0 -0
  73. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/instrumentation/file_instrumentation.py +0 -0
  74. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/__init__.py +0 -0
  75. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/extract.py +0 -0
  76. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/finder.py +0 -0
  77. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/java/__init__.py +0 -0
  78. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/__init__.py +0 -0
  79. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/extract.py +0 -0
  80. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/python/finder.py +0 -0
  81. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/language/visitor.py +0 -0
  82. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/logger.py +0 -0
  83. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/model/__init__.py +0 -0
  84. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/weights/__init__.py +0 -0
  85. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit/weights/analyzer.py +0 -0
  86. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/dependency_links.txt +0 -0
  87. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/entry_points.txt +0 -0
  88. {sflkit-0.5.6 → sflkit-1.0.0}/src/sflkit.egg-info/top_level.txt +0 -0
  89. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_analysis_objects.py +0 -0
  90. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_cli.py +0 -0
  91. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_color.py +0 -0
  92. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_config.py +0 -0
  93. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_coverage.py +0 -0
  94. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_dependency_weights.py +0 -0
  95. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_execution.py +0 -0
  96. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_expression.py +0 -0
  97. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_language.py +0 -0
  98. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_parallelization.py +0 -0
  99. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_predicates.py +0 -0
  100. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_runner_parallel.py +0 -0
  101. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_runner_pytest_versions.py +0 -0
  102. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_scope.py +0 -0
  103. {sflkit-0.5.6 → sflkit-1.0.0}/tests/test_spectra.py +0 -0
  104. {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.5.6
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.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.2
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 currently Python 3 but we plan on releasing further language support.
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 currently Python 3 but we plan on releasing further language support.
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.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.2",
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",
@@ -5,7 +5,7 @@ from sflkit.analysis.analyzer import Analyzer
5
5
  from sflkit.config import Config, parse_config
6
6
  from sflkit.instrumentation.dir_instrumentation import DirInstrumentation
7
7
 
8
- __version__ = "0.5.6"
8
+ __version__ = "1.0.0"
9
9
 
10
10
 
11
11
  def instrument_config(conf: Config):
@@ -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
- suspiciousness.append(suggestion.suspiciousness)
159
- if suggestion.suspiciousness not in suggestions:
160
- suggestions[suggestion.suspiciousness] = set(suggestion.lines)
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
- suggestions[suggestion.suspiciousness] |= set(suggestion.lines)
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
- return sorted(
173
- [
174
- Suggestion(list(lines), suspiciousness)
175
- for suspiciousness, lines in suggestions.items()
176
- ],
177
- reverse=True,
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