sflkit 0.3.5__tar.gz → 0.5.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 (82) hide show
  1. {sflkit-0.3.5/src/sflkit.egg-info → sflkit-0.5.0}/PKG-INFO +7 -26
  2. {sflkit-0.3.5 → sflkit-0.5.0}/pyproject.toml +13 -4
  3. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/__init__.py +7 -4
  4. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/analyzer.py +8 -1
  5. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/config.py +35 -0
  6. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/events/event_file.py +7 -1
  7. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/features/handler.py +4 -4
  8. sflkit-0.5.0/src/sflkit/model/parallel.py +127 -0
  9. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/runners/__init__.py +3 -0
  10. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/runners/run.py +149 -4
  11. {sflkit-0.3.5 → sflkit-0.5.0/src/sflkit.egg-info}/PKG-INFO +7 -26
  12. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit.egg-info/SOURCES.txt +3 -0
  13. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit.egg-info/requires.txt +1 -1
  14. sflkit-0.5.0/tests/test_parallelization.py +318 -0
  15. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_runner.py +2 -1
  16. sflkit-0.5.0/tests/test_runner_parallel.py +117 -0
  17. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_runner_pytest_versions.py +1 -1
  18. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_visitors.py +4 -4
  19. {sflkit-0.3.5 → sflkit-0.5.0}/LICENSE +0 -0
  20. {sflkit-0.3.5 → sflkit-0.5.0}/README.md +0 -0
  21. {sflkit-0.3.5 → sflkit-0.5.0}/setup.cfg +0 -0
  22. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/__init__.py +0 -0
  23. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/analysis_type.py +0 -0
  24. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/factory.py +0 -0
  25. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/mapping.py +0 -0
  26. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/predicate.py +0 -0
  27. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/similarity.py +0 -0
  28. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/spectra.py +0 -0
  29. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/analysis/suggestion.py +0 -0
  30. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/cli.py +0 -0
  31. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/color.py +0 -0
  32. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/evaluation.py +0 -0
  33. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/events/__init__.py +0 -0
  34. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/events/mapping.py +0 -0
  35. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/features/__init__.py +0 -0
  36. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/features/value.py +0 -0
  37. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/features/vector.py +0 -0
  38. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/instrumentation/__init__.py +0 -0
  39. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/instrumentation/dir_instrumentation.py +0 -0
  40. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/instrumentation/file_instrumentation.py +0 -0
  41. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/__init__.py +0 -0
  42. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/extract.py +0 -0
  43. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/finder.py +0 -0
  44. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/java/__init__.py +0 -0
  45. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/java/extract.py +0 -0
  46. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/java/factory.py +0 -0
  47. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/java/finder.py +0 -0
  48. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/java/visitor.py +0 -0
  49. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/language.py +0 -0
  50. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/meta.py +0 -0
  51. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/python/__init__.py +0 -0
  52. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/python/extract.py +0 -0
  53. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/python/factory.py +0 -0
  54. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/python/finder.py +0 -0
  55. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/python/visitor.py +0 -0
  56. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/language/visitor.py +0 -0
  57. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/logger.py +0 -0
  58. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/model/__init__.py +0 -0
  59. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/model/model.py +0 -0
  60. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/model/scope.py +0 -0
  61. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/weights/__init__.py +0 -0
  62. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/weights/analyzer.py +0 -0
  63. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit/weights/models.py +0 -0
  64. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit.egg-info/dependency_links.txt +0 -0
  65. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit.egg-info/entry_points.txt +0 -0
  66. {sflkit-0.3.5 → sflkit-0.5.0}/src/sflkit.egg-info/top_level.txt +0 -0
  67. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_analysis_objects.py +0 -0
  68. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_analyzer.py +0 -0
  69. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_cli.py +0 -0
  70. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_color.py +0 -0
  71. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_config.py +0 -0
  72. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_coverage.py +0 -0
  73. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_dependency_weights.py +0 -0
  74. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_evaluation.py +0 -0
  75. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_events.py +0 -0
  76. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_execution.py +0 -0
  77. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_instrumentation.py +0 -0
  78. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_language.py +0 -0
  79. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_predicates.py +0 -0
  80. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_scope.py +0 -0
  81. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_spectra.py +0 -0
  82. {sflkit-0.3.5 → sflkit-0.5.0}/tests/test_suggestions.py +0 -0
@@ -1,41 +1,21 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: sflkit
3
- Version: 0.3.5
3
+ Version: 0.5.0
4
4
  Summary: SFLKit: : A Workbench for Statistical Fault Localization
5
5
  Author-email: Marius Smytzek <marius.smytzek@cispa.de>
6
- License: MIT License
7
-
8
- Copyright (c) 2022 Marius Smytzek
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
6
  Project-URL: Homepage, https://github.com/smythi93/sflkit/
29
7
  Project-URL: Bug Tracker, https://github.com/smythi93/sflkit/issues
30
8
  Classifier: Intended Audience :: Science/Research
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.11
31
12
  Classifier: Programming Language :: Python :: 3.10
32
- Classifier: License :: OSI Approved :: MIT License
33
13
  Classifier: Topic :: Scientific/Engineering
34
14
  Classifier: Topic :: Software Development :: Testing
35
15
  Requires-Python: >=3.10
36
16
  Description-Content-Type: text/markdown
37
17
  License-File: LICENSE
38
- Requires-Dist: sflkitlib>=0.1.1
18
+ Requires-Dist: sflkitlib>=0.2.1
39
19
  Requires-Dist: numpy>=2.1.3
40
20
  Requires-Dist: matplotlib>=3.10.0
41
21
  Requires-Dist: pandas>=2.2.3
@@ -47,6 +27,7 @@ Requires-Dist: pytest-cov>=6.0.0; extra == "test"
47
27
  Requires-Dist: pytest-html>=4.1.1; extra == "test"
48
28
  Requires-Dist: pytest-rerunfailures>=15.0; extra == "test"
49
29
  Requires-Dist: parameterized>=0.9.0; extra == "test"
30
+ Dynamic: license-file
50
31
 
51
32
  # SFLKit: A Workbench for Statistical Fault Localization
52
33
 
@@ -7,23 +7,25 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "sflkit"
10
- version = "0.3.5"
10
+ dynamic = ["version"]
11
11
  authors = [
12
12
  { name = "Marius Smytzek", email = "marius.smytzek@cispa.de" },
13
13
  ]
14
14
  description = "SFLKit: : A Workbench for Statistical Fault Localization"
15
15
  readme = "README.md"
16
- license = { file = "LICENSE" }
16
+ license-files = ["LICENSE*"]
17
17
  requires-python = ">=3.10"
18
18
  classifiers = [
19
19
  "Intended Audience :: Science/Research",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.11",
20
23
  "Programming Language :: Python :: 3.10",
21
- "License :: OSI Approved :: MIT License",
22
24
  "Topic :: Scientific/Engineering",
23
25
  "Topic :: Software Development :: Testing"
24
26
  ]
25
27
  dependencies = [
26
- "sflkitlib>=0.1.1",
28
+ "sflkitlib>=0.2.1",
27
29
  "numpy>=2.1.3",
28
30
  "matplotlib>=3.10.0",
29
31
  "pandas>=2.2.3",
@@ -35,9 +37,16 @@ dependencies = [
35
37
  "Homepage" = "https://github.com/smythi93/sflkit/"
36
38
  "Bug Tracker" = "https://github.com/smythi93/sflkit/issues"
37
39
 
40
+ [tool.setuptools.dynamic]
41
+ version = {attr = "sflkit.__version__"}
42
+
38
43
  [project.scripts]
39
44
  sflkit = "sflkit.cli:main"
40
45
 
46
+ # only include packages in src/
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+
41
50
  [project.optional-dependencies]
42
51
  test = [
43
52
  "pytest>=8.3.4",
@@ -6,6 +6,9 @@ from sflkit.config import Config, parse_config
6
6
  from sflkit.instrumentation.dir_instrumentation import DirInstrumentation
7
7
 
8
8
 
9
+ __version__ = "0.5.0"
10
+
11
+
9
12
  def instrument_config(conf: Config):
10
13
  instrumentation = DirInstrumentation(
11
14
  conf.visitor, conf.mapping.path, test_visitor=conf.test_visitor
@@ -54,10 +57,10 @@ def analyze_config(conf: Config, analysis_dump: PathLike = None):
54
57
  results[analysis_type.name] = dict()
55
58
  for metric in conf.metrics:
56
59
  try:
57
- results[analysis_type.name][
58
- metric.__name__
59
- ] = analyzer.get_sorted_suggestions(
60
- conf.target_path, metric, analysis_type
60
+ results[analysis_type.name][metric.__name__] = (
61
+ analyzer.get_sorted_suggestions(
62
+ conf.target_path, metric, analysis_type
63
+ )
61
64
  )
62
65
  except (AttributeError, ValueError, TypeError) as e:
63
66
  raise e
@@ -22,6 +22,7 @@ from sflkit.analysis.spectra import Line, Function, DefUse, Loop, Length
22
22
  from sflkit.analysis.suggestion import Suggestion
23
23
  from sflkit.events.event_file import EventFile
24
24
  from sflkit.model.model import Model, MetaModel
25
+ from sflkit.model.parallel import ParallelModel
25
26
 
26
27
 
27
28
  class AnalysisEncoder(json.JSONEncoder):
@@ -39,7 +40,8 @@ class Analyzer:
39
40
  irrelevant_event_files: Optional[List[EventFile]] = None,
40
41
  factory: Optional[AnalysisFactory] = None,
41
42
  meta_model: Optional[MetaModel] = None,
42
- model_class: Type[Model] = Model,
43
+ model_class: Type[Model] = None,
44
+ parallel: bool = False,
43
45
  ):
44
46
  if (
45
47
  relevant_event_files is None
@@ -56,6 +58,11 @@ class Analyzer:
56
58
  if self.meta:
57
59
  self.model = meta_model
58
60
  else:
61
+ if model_class is None:
62
+ if parallel:
63
+ model_class = ParallelModel
64
+ else:
65
+ model_class = Model
59
66
  self.model = model_class(factory)
60
67
  self.paths: Dict[int, os.PathLike] = dict()
61
68
  self.max_suspiciousness = 0
@@ -58,6 +58,8 @@ class Config:
58
58
 
59
59
  [test]
60
60
  runner=TestRunner : The testrunner class, None if no run needed
61
+ workers=number_of_workers : Number of workers for parallel runners
62
+ thread_support=True|False : Whether the subject uses threading
61
63
  """
62
64
 
63
65
  def __init__(self, path: Union[str, configparser.ConfigParser] = None):
@@ -82,6 +84,8 @@ class Config:
82
84
  self.instrument_test_files = list()
83
85
  self.instrument_working = None
84
86
  self.runner = None
87
+ self.workers = 1
88
+ self.thread_support = False
85
89
  self.mapping = None
86
90
  self.mapping_path = None
87
91
  self.events_id_generator = IDGenerator()
@@ -233,6 +237,23 @@ class Config:
233
237
  test = config["test"]
234
238
  if "runner" in test and test["runner"] != "None":
235
239
  self.runner = RunnerType[test["runner"].upper()]
240
+ if "workers" in test:
241
+ self.workers = int(test["workers"])
242
+ if (
243
+ self.workers > 1
244
+ and self.runner
245
+ and not self.runner.name.startswith("PARALLEL_")
246
+ ):
247
+ self.runner = RunnerType["PARALLEL_" + self.runner.name]
248
+ if "thread_support" in test:
249
+ self.thread_support = test["thread_support"].lower() in [
250
+ "True",
251
+ "true",
252
+ "1",
253
+ "t",
254
+ "y",
255
+ "yes",
256
+ ]
236
257
 
237
258
  except KeyError as e:
238
259
  raise ConfigError(e)
@@ -258,6 +279,8 @@ class Config:
258
279
  instrument_test: Optional[List[str]] = None,
259
280
  instrument_working: Optional[str] = None,
260
281
  runner: Optional[RunnerType] = None,
282
+ workers: Optional[int] = 1,
283
+ thread_support: Optional[bool] = False,
261
284
  ):
262
285
  conf = Config()
263
286
  conf.target_path = target_path
@@ -280,6 +303,8 @@ class Config:
280
303
  conf.instrument_test = instrument_test or list()
281
304
  conf.instrument_working = instrument_working
282
305
  conf.runner = runner
306
+ conf.workers = workers or 1
307
+ conf.thread_support = thread_support
283
308
  if mapping:
284
309
  conf.mapping = mapping
285
310
  if mapping.path:
@@ -336,6 +361,8 @@ class Config:
336
361
  tests=None,
337
362
  test_files=None,
338
363
  runner=None,
364
+ workers=None,
365
+ thread_support=None,
339
366
  ):
340
367
  conf = configparser.ConfigParser()
341
368
  conf["target"] = dict()
@@ -375,6 +402,10 @@ class Config:
375
402
  conf["instrumentation"]["test_files"] = test_files
376
403
  if runner:
377
404
  conf["test"]["runner"] = runner
405
+ if workers:
406
+ conf["test"]["workers"] = str(workers)
407
+ if thread_support:
408
+ conf["test"]["thread_support"] = str(thread_support)
378
409
 
379
410
  return Config(conf)
380
411
 
@@ -425,6 +456,10 @@ class Config:
425
456
  )
426
457
  if self.runner:
427
458
  conf["test"]["runner"] = self.runner.name
459
+ if self.workers:
460
+ conf["test"]["workers"] = str(self.workers)
461
+ if self.thread_support:
462
+ conf["test"]["thread_support"] = str(self.thread_support)
428
463
 
429
464
  with open(path, "w") as fp:
430
465
  conf.write(fp)
@@ -13,11 +13,13 @@ class EventFile(object):
13
13
  run_id: int,
14
14
  mapping: EventMapping,
15
15
  failing: bool = False,
16
+ thread_support: bool = False,
16
17
  ):
17
18
  self.path = path
18
19
  self.run_id = run_id
19
20
  self.mapping = mapping
20
21
  self.failing = failing
22
+ self.thread_support = thread_support
21
23
  self._csv_reader = None
22
24
  self._file_pointer = None
23
25
 
@@ -37,7 +39,11 @@ class EventFile(object):
37
39
  def load(self):
38
40
  while self._file_pointer.peek(1):
39
41
  try:
40
- e = event.load_next_event(self._file_pointer, self.mapping.mapping)
42
+ e = event.load_next_event(
43
+ self._file_pointer,
44
+ self.mapping.mapping,
45
+ with_thread_id=self.thread_support,
46
+ )
41
47
  if self.mapping.is_valid(e):
42
48
  yield e
43
49
  except (IndexError, ValueError, PickleError, KeyError):
@@ -78,14 +78,14 @@ class FeatureBuilder(CombinationFactory):
78
78
  def hit(self, id_, *args, **kwargs):
79
79
  event: Event
80
80
  for a in self.analysis:
81
- if isinstance(a, Spectrum):
82
- feature = BinaryFeature(str(a), a)
81
+ if isinstance(a, Predicate):
82
+ feature = TertiaryFeature(str(a), a)
83
83
  self.feature_vectors[id_].set_feature(
84
84
  feature, self.map_evaluation(a, id_)
85
85
  )
86
86
  self.all_features.add(feature)
87
- elif isinstance(a, Predicate):
88
- feature = TertiaryFeature(str(a), a)
87
+ elif isinstance(a, Spectrum):
88
+ feature = BinaryFeature(str(a), a)
89
89
  self.feature_vectors[id_].set_feature(
90
90
  feature, self.map_evaluation(a, id_)
91
91
  )
@@ -0,0 +1,127 @@
1
+ from typing import Set, List, Optional
2
+
3
+ from sflkit.analysis.analysis_type import AnalysisObject
4
+ from sflkit.events.event_file import EventFile
5
+ from sflkit.model.model import Model
6
+ from sflkit.model.scope import Scope
7
+
8
+
9
+ class ParallelModel(Model):
10
+ def __init__(self, factory):
11
+ super().__init__(factory)
12
+ self.variables_map: dict[int, Scope] = dict()
13
+ self.returns_map: dict[int, Scope] = dict()
14
+
15
+ def prepare(self, event_file):
16
+ self.factory.reset()
17
+ self.variables = Scope()
18
+ self.returns = Scope()
19
+ self.current_event_file = event_file
20
+ self.variables_map = dict()
21
+ self.returns_map = dict()
22
+
23
+ def follow_up(self, event_file):
24
+ pass
25
+
26
+ def handle_line_event(self, event):
27
+ self.handle_event(event)
28
+
29
+ def handle_branch_event(self, event):
30
+ self.handle_event(event)
31
+
32
+ def handle_function_enter_event(self, event):
33
+ if event.thread_id is None:
34
+ self.enter_scope()
35
+ else:
36
+ self.enter_parallel_scope(event.thread_id)
37
+ self.handle_event(event)
38
+
39
+ def handle_function_exit_event(self, event):
40
+ if event.thread_id is None:
41
+ self.returns.add(event.function, event.return_value, event.type_)
42
+ else:
43
+ self.returns_map.setdefault(event.thread_id, Scope()).add(
44
+ event.function, event.return_value, event.type_
45
+ )
46
+ self.handle_event(event, self.returns)
47
+ if event.thread_id is None:
48
+ self.exit_scope()
49
+ else:
50
+ self.exit_parallel_scope(event.thread_id)
51
+
52
+ def handle_function_error_event(self, event):
53
+ self.handle_event(event)
54
+ if event.thread_id is None:
55
+ self.exit_scope()
56
+ else:
57
+ self.exit_parallel_scope(event.thread_id)
58
+
59
+ def handle_def_event(self, event):
60
+ if event.thread_id is None:
61
+ self.variables.add(event.var, event.value, event.type_)
62
+ else:
63
+ self.variables_map.setdefault(event.thread_id, self.variables).add(
64
+ event.var, event.value, event.type_
65
+ )
66
+ self.handle_event(event, self.variables)
67
+
68
+ def handle_use_event(self, event):
69
+ if event.thread_id is None:
70
+ self.handle_event(event, self.variables)
71
+ else:
72
+ self.handle_event(
73
+ event, self.variables_map.get(event.thread_id, self.variables)
74
+ )
75
+
76
+ def handle_condition_event(self, event):
77
+ self.handle_event(event)
78
+
79
+ def handle_loop_begin_event(self, event):
80
+ self.handle_event(event)
81
+
82
+ def handle_loop_hit_event(self, event):
83
+ self.handle_event(event)
84
+
85
+ def handle_loop_end_event(self, event):
86
+ self.handle_event(event)
87
+
88
+ def handle_len_event(self, event):
89
+ self.handle_event(event)
90
+
91
+ def handle_test_start_event(self, event):
92
+ pass
93
+
94
+ def handle_test_end_event(self, event):
95
+ pass
96
+
97
+ def handle_test_line_event(self, event):
98
+ pass
99
+
100
+ def handle_test_def_event(self, event):
101
+ pass
102
+
103
+ def handle_test_use_event(self, event):
104
+ pass
105
+
106
+ def handle_test_assert_event(self, event):
107
+ pass
108
+
109
+ def enter_parallel_scope(self, thread_id: int):
110
+ self.variables_map[thread_id] = self.variables_map.get(
111
+ thread_id, self.variables
112
+ ).enter()
113
+
114
+ def exit_parallel_scope(self, thread_id: int):
115
+ self.variables_map[thread_id] = self.variables_map.get(
116
+ thread_id, self.variables
117
+ ).exit()
118
+
119
+ # noinspection PyUnresolvedReferences
120
+ def get_analysis(self) -> Set[AnalysisObject]:
121
+ return self.factory.get_all()
122
+
123
+ def finalize(
124
+ self, passed: Optional[List[EventFile]], failed: Optional[List[EventFile]]
125
+ ):
126
+ for p in self.get_analysis():
127
+ p.analyze(passed, failed)
@@ -6,6 +6,7 @@ from sflkit.runners.run import (
6
6
  PytestRunner,
7
7
  UnittestRunner,
8
8
  InputRunner,
9
+ ParallelPytestRunner,
9
10
  )
10
11
 
11
12
 
@@ -17,3 +18,5 @@ class RunnerType(enum.Enum):
17
18
  PYTEST_RUNNER = PytestRunner
18
19
  UNITTEST_RUNNER = UnittestRunner
19
20
  INPUT_RUNNER = InputRunner
21
+ PARALLEL_PYTEST_RUNNER = ParallelPytestRunner
22
+ PARALLEL_UNITTEST_RUNNER = UnittestRunner
@@ -6,6 +6,8 @@ import re
6
6
  import shutil
7
7
  import string
8
8
  import subprocess
9
+ import threading
10
+ from concurrent.futures import ThreadPoolExecutor
9
11
  from pathlib import Path
10
12
  from typing import List, Dict, Optional, Tuple, Set
11
13
 
@@ -35,7 +37,13 @@ class TestResult(enum.Enum):
35
37
 
36
38
 
37
39
  class Runner(abc.ABC):
38
- def __init__(self, re_filter: str = r".*", timeout=DEFAULT_TIMEOUT):
40
+ def __init__(
41
+ self,
42
+ re_filter: str = r".*",
43
+ timeout=DEFAULT_TIMEOUT,
44
+ is_parallel: bool = False,
45
+ thread_support: bool = False,
46
+ ):
39
47
  self.timeout = timeout
40
48
  self.re_filter = re.compile(re_filter)
41
49
  self.passing_tests = set()
@@ -46,6 +54,12 @@ class Runner(abc.ABC):
46
54
  TestResult.FAILING: self.failing_tests,
47
55
  TestResult.UNDEFINED: self.undefined_tests,
48
56
  }
57
+ self.is_parallel = is_parallel
58
+ self.thread_support = thread_support
59
+
60
+ @staticmethod
61
+ def use_parallel() -> "Runner":
62
+ raise NotImplementedError()
49
63
 
50
64
  def get_tests(
51
65
  self,
@@ -94,6 +108,10 @@ class Runner(abc.ABC):
94
108
  directory / "EVENTS_PATH",
95
109
  output / test_result.get_dir() / self.safe(test),
96
110
  )
111
+ else:
112
+ LOGGER.warning(f"EVENTS_PATH not found for test {test}")
113
+ # Ensure all files are flushed to disk (helps with race conditions in CI)
114
+ os.sync()
97
115
 
98
116
  def run(
99
117
  self,
@@ -107,6 +125,9 @@ class Runner(abc.ABC):
107
125
  self.passing_tests.clear()
108
126
  self.failing_tests.clear()
109
127
  self.undefined_tests.clear()
128
+ if environ is None:
129
+ environ = os.environ.copy()
130
+ environ["EVENTS_THREADS"] = "1" if self.thread_support else "0"
110
131
  self.run_tests(
111
132
  directory,
112
133
  output,
@@ -118,7 +139,9 @@ class Runner(abc.ABC):
118
139
 
119
140
 
120
141
  class VoidRunner(Runner):
121
- pass
142
+ @staticmethod
143
+ def use_parallel() -> Runner:
144
+ return VoidRunner
122
145
 
123
146
 
124
147
  class PytestStructure:
@@ -262,10 +285,15 @@ class PytestRunner(Runner):
262
285
  re_filter: str = r".*",
263
286
  timeout=DEFAULT_TIMEOUT,
264
287
  set_python_path: bool = False,
288
+ thread_support: bool = False,
265
289
  ):
266
- super().__init__(re_filter, timeout)
290
+ super().__init__(re_filter, timeout, thread_support=thread_support)
267
291
  self.set_python_path = set_python_path
268
292
 
293
+ @staticmethod
294
+ def use_parallel() -> Runner:
295
+ return ParallelPytestRunner
296
+
269
297
  @staticmethod
270
298
  def common_base(directory: Path, tests: List[str]) -> Path:
271
299
  parts = directory.parts
@@ -401,6 +429,7 @@ class PytestRunner(Runner):
401
429
  cwd=directory,
402
430
  )
403
431
  LOGGER.debug(f"pytest collection finished with {process.returncode}")
432
+ LOGGER.debug(process.stderr.decode("utf8"))
404
433
  tests = PytestStructure.parse_tests(process.stdout.decode("utf8"))
405
434
  LOGGER.debug(f"pytest collection found {len(tests)} tests")
406
435
  return self.normalize_paths(tests, file_bases, directory, root_dir)
@@ -460,13 +489,18 @@ class InputRunner(Runner):
460
489
  access: os.PathLike,
461
490
  passing: List[str | List[str]],
462
491
  failing: List[str | List[str]],
492
+ thread_support: bool = False,
463
493
  ):
464
- super().__init__()
494
+ super().__init__(thread_support=thread_support)
465
495
  self.access = access
466
496
  self.passing: Dict[str, List[str]] = self._prepare_tests(passing, "passing")
467
497
  self.failing: Dict[str, List[str]] = self._prepare_tests(failing, "failing")
468
498
  self.output: Dict[str, Tuple[str, str]] = dict()
469
499
 
500
+ @staticmethod
501
+ def use_parallel() -> Runner:
502
+ return ParallelInputRunner
503
+
470
504
  @staticmethod
471
505
  def split(s: str, sep: str = ",", esc: str = "\"'"):
472
506
  values = list()
@@ -526,3 +560,114 @@ class InputRunner(Runner):
526
560
  process.stderr.decode("utf8"),
527
561
  )
528
562
  return result
563
+
564
+
565
+ class ParallelPytestRunner(PytestRunner):
566
+ def __init__(
567
+ self,
568
+ re_filter: str = r".*",
569
+ timeout=DEFAULT_TIMEOUT,
570
+ set_python_path: bool = False,
571
+ workers: int = 4,
572
+ thread_support: bool = False,
573
+ ):
574
+ super().__init__(
575
+ re_filter, timeout, set_python_path, thread_support=thread_support
576
+ )
577
+ self.workers = workers
578
+ self.is_parallel = True
579
+
580
+ @staticmethod
581
+ def use_parallel() -> Runner:
582
+ return ParallelPytestRunner
583
+
584
+ def run_tests(
585
+ self,
586
+ directory: Path,
587
+ output: Path,
588
+ tests: List[str],
589
+ environ: Environment = None,
590
+ ):
591
+ output.mkdir(parents=True, exist_ok=True)
592
+ for test_result in TestResult:
593
+ (output / test_result.get_dir()).mkdir(parents=True, exist_ok=True)
594
+
595
+ def process_test(test: str):
596
+ if environ is None:
597
+ local_environ = os.environ.copy()
598
+ else:
599
+ local_environ = environ.copy()
600
+ events_path_name = f"EVENTS_PATH_{threading.get_ident()}"
601
+ local_environ["EVENTS_PATH"] = events_path_name
602
+ tr = self.run_test(directory, test, environ=local_environ)
603
+ self.tests[tr].add(test)
604
+ if os.path.exists(directory / events_path_name):
605
+ shutil.move(
606
+ directory / events_path_name,
607
+ output / tr.get_dir() / self.safe(test),
608
+ )
609
+ else:
610
+ LOGGER.warning(f"EVENTS_PATH not found for test {test}")
611
+
612
+ with ThreadPoolExecutor(max_workers=self.workers) as executor:
613
+ # Consume the iterator to ensure all tasks complete
614
+ list(executor.map(process_test, tests))
615
+
616
+ # Ensure all files are flushed to disk (helps with race conditions in CI)
617
+ os.sync()
618
+
619
+
620
+ class ParallelInputRunner(InputRunner):
621
+ def __init__(
622
+ self,
623
+ access: os.PathLike,
624
+ passing: List[str | List[str]],
625
+ failing: List[str | List[str]],
626
+ workers: int = 4,
627
+ thread_support: bool = False,
628
+ ):
629
+ super().__init__(access, passing, failing, thread_support=thread_support)
630
+ self.workers = workers
631
+ self.is_parallel = True
632
+
633
+ @staticmethod
634
+ def use_parallel() -> Runner:
635
+ return ParallelInputRunner
636
+
637
+ def run_tests(
638
+ self,
639
+ directory: Path,
640
+ output: Path,
641
+ tests: List[str],
642
+ environ: Environment = None,
643
+ ):
644
+ output.mkdir(parents=True, exist_ok=True)
645
+ for test_result in TestResult:
646
+ (output / test_result.get_dir()).mkdir(parents=True, exist_ok=True)
647
+
648
+ lock = threading.Lock()
649
+
650
+ def process_test(test_name: str):
651
+ if environ is None:
652
+ local_environ = os.environ.copy()
653
+ else:
654
+ local_environ = environ.copy()
655
+ events_path_name = f"EVENTS_PATH_{threading.get_ident()}"
656
+ local_environ["EVENTS_PATH"] = events_path_name
657
+ tr = self.run_test(directory, test_name, environ=local_environ)
658
+ with lock:
659
+ self.tests[tr].add(test_name)
660
+ if os.path.exists(directory / events_path_name):
661
+ shutil.move(
662
+ directory / events_path_name,
663
+ output / tr.get_dir() / self.safe(test_name),
664
+ )
665
+ else:
666
+ LOGGER.warning(f"EVENTS_PATH not found for test {test_name}")
667
+
668
+ with ThreadPoolExecutor(max_workers=self.workers) as executor:
669
+ # Consume the iterator to ensure all tasks complete
670
+ list(executor.map(process_test, tests))
671
+
672
+ # Ensure all files are flushed to disk (helps with race conditions in CI)
673
+ os.sync()