flamapy 2.6.0.dev3__tar.gz → 2.6.0.dev5__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 (22) hide show
  1. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/PKG-INFO +8 -6
  2. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy/commands/__init__.py +24 -11
  3. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy/interfaces/python/flamapy_feature_model.py +87 -3
  4. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/PKG-INFO +8 -6
  5. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/SOURCES.txt +2 -1
  6. flamapy-2.6.0.dev5/flamapy.egg-info/requires.txt +15 -0
  7. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/pyproject.toml +10 -6
  8. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_basic_units.py +1 -1
  9. flamapy-2.6.0.dev5/tests/test_new_operations.py +110 -0
  10. flamapy-2.6.0.dev3/flamapy.egg-info/requires.txt +0 -12
  11. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/LICENSE +0 -0
  12. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/README.md +0 -0
  13. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy/interfaces/python/__init__.py +0 -0
  14. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/dependency_links.txt +0 -0
  15. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/entry_points.txt +0 -0
  16. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/top_level.txt +0 -0
  17. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/setup.cfg +0 -0
  18. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/setup.py +0 -0
  19. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_cli_basic_units.py +0 -0
  20. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_documentation_examples.py +0 -0
  21. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_fama_suite_bdd.py +0 -0
  22. {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_fama_suite_sat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy
3
- Version: 2.6.0.dev3
3
+ Version: 2.6.0.dev5
4
4
  Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,17 +8,19 @@ Project-URL: Homepage, https://github.com/flamapy/flamapy-feature-model
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: flamapy-fw~=2.6.0.dev3
12
- Requires-Dist: flamapy-fm~=2.6.0.dev3
13
- Requires-Dist: flamapy-sat~=2.6.0.dev3
14
- Requires-Dist: flamapy-bdd~=2.6.0.dev3
15
- Requires-Dist: flamapy-z3~=2.6.0.dev3
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev5
12
+ Requires-Dist: flamapy-fm~=2.6.0.dev5
13
+ Requires-Dist: flamapy-sat~=2.6.0.dev5
14
+ Requires-Dist: flamapy-bdd~=2.6.0.dev5
15
+ Requires-Dist: flamapy-z3~=2.6.0.dev5
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest; extra == "dev"
18
18
  Requires-Dist: pytest-mock; extra == "dev"
19
19
  Requires-Dist: prospector; extra == "dev"
20
20
  Requires-Dist: mypy; extra == "dev"
21
21
  Requires-Dist: coverage; extra == "dev"
22
+ Provides-Extra: sharpsat
23
+ Requires-Dist: flamapy-sharpsat~=2.6.0.dev4; extra == "sharpsat"
22
24
  Dynamic: license-file
23
25
 
24
26
  # flamapy
@@ -5,7 +5,7 @@ from functools import wraps
5
5
  import inspect
6
6
  from pathlib import Path
7
7
  from shutil import copytree
8
- from typing import Any, List, Tuple, Optional
8
+ from typing import Any, List, Tuple, Optional, Union, get_args, get_origin
9
9
  from types import FunctionType
10
10
 
11
11
  from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
@@ -92,30 +92,43 @@ def generate_plugin(args): # type: ignore
92
92
  print("Plugin generated!")
93
93
 
94
94
 
95
+ def resolve_annotation_type(annotation: Any) -> type:
96
+ """Map a parameter annotation to a concrete type usable as an argparse 'type'.
97
+
98
+ Unwraps Optional[X]/Union[X, None] to X; falls back to str for anything
99
+ that is not a plain class (argparse requires a callable).
100
+ """
101
+ if annotation is inspect.Parameter.empty:
102
+ return str
103
+ if get_origin(annotation) is Union:
104
+ args = [arg for arg in get_args(annotation) if arg is not type(None)]
105
+ annotation = args[0] if args else str
106
+ return annotation if isinstance(annotation, type) else str
107
+
108
+
95
109
  def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
96
110
  for name, docstring, method, parameters in dynamic_commands:
97
111
  subparser = subparsers.add_parser(name, help=docstring)
98
112
  subparser.add_argument("model_path", type=str, help="Path to the feature model file")
99
- if "configuration_path" in [param.name for param in parameters]:
100
- subparser.add_argument(
101
- "--configuration_path",
102
- type=str,
103
- help="Path to the configuration file",
104
- required=False,
105
- )
106
113
  for param in parameters:
107
114
  arg_name = param.name
108
115
  if arg_name not in ["model_path"]: # Avoid duplicates
116
+ arg_type = resolve_annotation_type(param.annotation)
109
117
  if param.default == param.empty: # Positional argument
118
+ subparser.add_argument(arg_name, type=arg_type, help=arg_type.__name__)
119
+ elif arg_type is bool: # Boolean flag (type=bool would parse 'False' as True)
110
120
  subparser.add_argument(
111
- arg_name, type=param.annotation, help=param.annotation.__name__
121
+ f"--{arg_name}",
122
+ action=argparse.BooleanOptionalAction,
123
+ default=param.default,
124
+ help="Optional bool",
112
125
  )
113
126
  else: # Optional argument
114
127
  subparser.add_argument(
115
128
  f"--{arg_name}",
116
- type=param.annotation,
129
+ type=arg_type,
117
130
  default=param.default,
118
- help=f"Optional {param.annotation.__name__}",
131
+ help=f"Optional {arg_type.__name__}",
119
132
  )
120
133
  subparser.set_defaults(func=method, method_name=name, parameters=parameters)
121
134
 
@@ -5,6 +5,7 @@ from flamapy.core.discover import DiscoverMetamodels
5
5
  from flamapy.core.models import VariabilityModel
6
6
  from flamapy.metamodels.fm_metamodel.models import FeatureModel
7
7
  from flamapy.core.exceptions import FlamaException
8
+ from flamapy.core.operations import OptimizationGoal
8
9
  from flamapy.metamodels.configuration_metamodel.models import Configuration
9
10
 
10
11
  logger = logging.getLogger(__name__)
@@ -16,6 +17,7 @@ class Backend(str, Enum):
16
17
  SAT = "sat"
17
18
  BDD = "bdd"
18
19
  Z3 = "z3"
20
+ SHARPSAT = "sharpsat"
19
21
 
20
22
 
21
23
  class FLAMAFeatureModel:
@@ -24,6 +26,7 @@ class FLAMAFeatureModel:
24
26
  Backend.SAT: ("_transform_to_sat", "sat_model"),
25
27
  Backend.BDD: ("_transform_to_bdd", "bdd_model"),
26
28
  Backend.Z3: ("_transform_to_z3", "z3_model"),
29
+ Backend.SHARPSAT: ("_transform_to_sharpsat", "sharpsat_model"),
27
30
  }
28
31
 
29
32
  def __init__(self, model_path: str, backend: Optional[str] = None):
@@ -42,6 +45,7 @@ class FLAMAFeatureModel:
42
45
  self.sat_model = None
43
46
  self.bdd_model = None
44
47
  self.z3_model = None
48
+ self.sharpsat_model = None
45
49
  self.diagnosis_model = None
46
50
  self.backend = Backend(backend) if backend is not None else None
47
51
 
@@ -84,6 +88,14 @@ class FLAMAFeatureModel:
84
88
  if self.z3_model is None:
85
89
  self.z3_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "z3")
86
90
 
91
+ def _transform_to_sharpsat(self) -> None:
92
+ if self.sharpsat_model is None:
93
+ # Requires the optional flamapy-sharpsat plugin. use_transformation_m2m raises a
94
+ # FlamaException if it is not installed, which the operations surface to the caller.
95
+ self.sharpsat_model = self.discover_metamodel.use_transformation_m2m(
96
+ self.fm_model, "sharpsat"
97
+ )
98
+
87
99
  def _transform_to_diagnosis(self) -> None:
88
100
  if self.diagnosis_model is None:
89
101
  self.diagnosis_model = self.discover_metamodel.use_transformation_m2m(
@@ -338,7 +350,8 @@ class FLAMAFeatureModel:
338
350
  combinations of features, taking into account the constraints and
339
351
  dependencies between features.
340
352
 
341
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to bdd.
353
+ ``backend`` selects the analysis plugin ("sat", "bdd", "z3", or "sharpsat" for a
354
+ scalable approximate count via the optional flamapy-sharpsat plugin); defaults to bdd.
342
355
  """
343
356
  try:
344
357
  model = self._select_backend(backend, Backend.BDD)
@@ -604,8 +617,9 @@ class FLAMAFeatureModel:
604
617
  Returns a random sample of valid configurations of the given size. When
605
618
  with_replacement is True, the same configuration may appear more than once.
606
619
 
607
- ``backend`` selects the analysis plugin; defaults to bdd. Only sat and bdd
608
- implement sampling.
620
+ ``backend`` selects the analysis plugin; defaults to bdd. sat and bdd implement
621
+ (deterministic/enumerating) sampling; "sharpsat" provides almost-uniform sampling
622
+ via the optional flamapy-sharpsat plugin.
609
623
  """
610
624
  try:
611
625
  model = self._select_backend(backend, Backend.BDD)
@@ -618,6 +632,76 @@ class FLAMAFeatureModel:
618
632
  logger.error("Error: %s", exception)
619
633
  return None
620
634
 
635
+ def t_wise_sampling(
636
+ self, t: int = 2, backend: Optional[str] = None
637
+ ) -> Union[None, List[Configuration]]:
638
+ """
639
+ Returns a t-wise (combinatorial) sample: a set of valid configurations that covers
640
+ every satisfiable combination of ``t`` feature selections (pairwise for ``t = 2``).
641
+
642
+ ``backend`` selects the analysis plugin; only "sat" implements t-wise sampling.
643
+ """
644
+ try:
645
+ model = self._select_backend(backend, Backend.SAT)
646
+ operation = self.discover_metamodel.get_operation(model, "PySATTWiseSampling")
647
+ operation.set_t(t)
648
+ operation.execute(model)
649
+ return operation.get_result()
650
+ except FlamaException as exception:
651
+ logger.error("Error: %s", exception)
652
+ return None
653
+
654
+ def minimum_configuration(
655
+ self, backend: Optional[str] = None
656
+ ) -> Union[None, Configuration]:
657
+ """
658
+ Returns a valid configuration with the fewest selected features (the minimum
659
+ working configuration).
660
+
661
+ ``backend`` selects the analysis plugin; only "sat" implements this operation.
662
+ """
663
+ try:
664
+ model = self._select_backend(backend, Backend.SAT)
665
+ operation = self.discover_metamodel.get_operation(model, "PySATMinimumConfiguration")
666
+ operation.execute(model)
667
+ return operation.get_result()
668
+ except FlamaException as exception:
669
+ logger.error("Error: %s", exception)
670
+ return None
671
+
672
+ def attribute_optimization(
673
+ self, objectives: Any, backend: Optional[str] = None
674
+ ) -> Union[None, List[Configuration]]:
675
+ """
676
+ Returns the configuration(s) that optimize one or more numeric feature attributes.
677
+
678
+ ``objectives`` is either a single attribute name (defaults to minimizing it) or a
679
+ mapping ``{attribute_name: "Minimize"|"Maximize"}``. ``backend`` selects the plugin:
680
+ "sat" performs single-objective MaxSAT optimization; "z3" additionally supports
681
+ typed attributes and multi-objective (Pareto) optimization. Defaults to "sat".
682
+ """
683
+ def _goal(value: Any) -> OptimizationGoal:
684
+ return (OptimizationGoal.MINIMIZE
685
+ if str(value).lower().startswith('min')
686
+ else OptimizationGoal.MAXIMIZE)
687
+
688
+ if isinstance(objectives, str):
689
+ attributes = {objectives: OptimizationGoal.MINIMIZE}
690
+ else:
691
+ attributes = {name: _goal(goal) for name, goal in dict(objectives).items()}
692
+
693
+ try:
694
+ model = self._select_backend(backend, Backend.SAT)
695
+ operation = self.discover_metamodel.get_operation(model, "AttributeOptimization")
696
+ operation.set_attributes(attributes)
697
+ operation.execute(model)
698
+ # z3 returns (configuration, values) tuples; sat returns configurations.
699
+ return [item[0] if isinstance(item, tuple) else item
700
+ for item in operation.get_result()]
701
+ except FlamaException as exception:
702
+ logger.error("Error: %s", exception)
703
+ return None
704
+
621
705
  # Z3-based operations
622
706
 
623
707
  def all_feature_bounds(self) -> Union[None, Dict[str, Dict[str, Any]]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy
3
- Version: 2.6.0.dev3
3
+ Version: 2.6.0.dev5
4
4
  Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,17 +8,19 @@ Project-URL: Homepage, https://github.com/flamapy/flamapy-feature-model
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: flamapy-fw~=2.6.0.dev3
12
- Requires-Dist: flamapy-fm~=2.6.0.dev3
13
- Requires-Dist: flamapy-sat~=2.6.0.dev3
14
- Requires-Dist: flamapy-bdd~=2.6.0.dev3
15
- Requires-Dist: flamapy-z3~=2.6.0.dev3
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev5
12
+ Requires-Dist: flamapy-fm~=2.6.0.dev5
13
+ Requires-Dist: flamapy-sat~=2.6.0.dev5
14
+ Requires-Dist: flamapy-bdd~=2.6.0.dev5
15
+ Requires-Dist: flamapy-z3~=2.6.0.dev5
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest; extra == "dev"
18
18
  Requires-Dist: pytest-mock; extra == "dev"
19
19
  Requires-Dist: prospector; extra == "dev"
20
20
  Requires-Dist: mypy; extra == "dev"
21
21
  Requires-Dist: coverage; extra == "dev"
22
+ Provides-Extra: sharpsat
23
+ Requires-Dist: flamapy-sharpsat~=2.6.0.dev4; extra == "sharpsat"
22
24
  Dynamic: license-file
23
25
 
24
26
  # flamapy
@@ -15,4 +15,5 @@ tests/test_basic_units.py
15
15
  tests/test_cli_basic_units.py
16
16
  tests/test_documentation_examples.py
17
17
  tests/test_fama_suite_bdd.py
18
- tests/test_fama_suite_sat.py
18
+ tests/test_fama_suite_sat.py
19
+ tests/test_new_operations.py
@@ -0,0 +1,15 @@
1
+ flamapy-fw~=2.6.0.dev5
2
+ flamapy-fm~=2.6.0.dev5
3
+ flamapy-sat~=2.6.0.dev5
4
+ flamapy-bdd~=2.6.0.dev5
5
+ flamapy-z3~=2.6.0.dev5
6
+
7
+ [dev]
8
+ pytest
9
+ pytest-mock
10
+ prospector
11
+ mypy
12
+ coverage
13
+
14
+ [sharpsat]
15
+ flamapy-sharpsat~=2.6.0.dev4
@@ -4,18 +4,18 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flamapy"
7
- version = "2.6.0.dev3"
7
+ version = "2.6.0.dev5"
8
8
  description = "Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
11
11
  authors = [{ name = "Flamapy", email = "flamapy@us.es" }]
12
12
  requires-python = ">=3.11"
13
13
  dependencies = [
14
- "flamapy-fw~=2.6.0.dev3",
15
- "flamapy-fm~=2.6.0.dev3",
16
- "flamapy-sat~=2.6.0.dev3",
17
- "flamapy-bdd~=2.6.0.dev3",
18
- "flamapy-z3~=2.6.0.dev3",
14
+ "flamapy-fw~=2.6.0.dev5",
15
+ "flamapy-fm~=2.6.0.dev5",
16
+ "flamapy-sat~=2.6.0.dev5",
17
+ "flamapy-bdd~=2.6.0.dev5",
18
+ "flamapy-z3~=2.6.0.dev5",
19
19
  ]
20
20
 
21
21
  [project.optional-dependencies]
@@ -26,6 +26,10 @@ dev = [
26
26
  "mypy",
27
27
  "coverage",
28
28
  ]
29
+ # Optional scalable counting/sampling backend (enables backend="sharpsat").
30
+ sharpsat = [
31
+ "flamapy-sharpsat~=2.6.0.dev4",
32
+ ]
29
33
 
30
34
  [project.scripts]
31
35
  flamapy = "flamapy.commands:flamapy_cli"
@@ -233,7 +233,7 @@ def test_commonality():
233
233
  result = flamafm.commonality(VALID_CONFIG)
234
234
 
235
235
  # Assert
236
- assert 0.0 == 0.0
236
+ assert result == 1.0
237
237
 
238
238
 
239
239
  def test_valid_configuration():
@@ -0,0 +1,110 @@
1
+ """Facade tests for the newly exposed operations: attribute optimization, minimum
2
+ configuration, t-wise sampling, and the sharpsat backend."""
3
+ import itertools
4
+ import os
5
+ import tempfile
6
+
7
+ import pytest
8
+
9
+ from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
10
+
11
+ _ATTRIBUTED_UVL = """features
12
+ Root {abstract}
13
+ optional
14
+ A {Cost 3}
15
+ B {Cost 5}
16
+ C {Cost 2}
17
+ constraints
18
+ A | B
19
+ """
20
+
21
+ _PLAIN_UVL = """features
22
+ Root {abstract}
23
+ optional
24
+ A
25
+ B
26
+ C
27
+ D
28
+ constraints
29
+ (A | B) => (C <=> D)
30
+ A => !B
31
+ """
32
+
33
+
34
+ def _model(uvl):
35
+ handle, path = tempfile.mkstemp(suffix='.uvl')
36
+ with os.fdopen(handle, 'w') as file:
37
+ file.write(uvl)
38
+ return path
39
+
40
+
41
+ def _sel(configuration):
42
+ return {name for name, value in configuration.elements.items() if value}
43
+
44
+
45
+ # --- attribute optimization -------------------------------------------------
46
+
47
+ def test_attribute_optimization_sat_minimize():
48
+ fm = FLAMAFeatureModel(_model(_ATTRIBUTED_UVL))
49
+ result = fm.attribute_optimization("Cost", backend="sat")
50
+ assert [_sel(c) for c in result] == [{"A", "Root"}]
51
+
52
+
53
+ def test_attribute_optimization_sat_maximize():
54
+ fm = FLAMAFeatureModel(_model(_ATTRIBUTED_UVL))
55
+ result = fm.attribute_optimization({"Cost": "Maximize"}, backend="sat")
56
+ assert [_sel(c) for c in result] == [{"A", "B", "C", "Root"}]
57
+
58
+
59
+ def test_attribute_optimization_sat_matches_z3():
60
+ path = _model(_ATTRIBUTED_UVL)
61
+ sat = FLAMAFeatureModel(path).attribute_optimization({"Cost": "Minimize"}, backend="sat")
62
+ z3 = FLAMAFeatureModel(path).attribute_optimization({"Cost": "Minimize"}, backend="z3")
63
+ assert {frozenset(_sel(c)) for c in sat} == {frozenset(_sel(c)) for c in z3}
64
+
65
+
66
+ # --- minimum configuration --------------------------------------------------
67
+
68
+ def test_minimum_configuration():
69
+ # The attributed model has an "A | B" constraint, so the minimum working
70
+ # configuration is Root plus exactly one of A/B.
71
+ fm = FLAMAFeatureModel(_model(_ATTRIBUTED_UVL))
72
+ selected = _sel(fm.minimum_configuration(backend="sat"))
73
+ assert "Root" in selected
74
+ assert len(selected) == 2 and len(selected & {"A", "B"}) == 1
75
+
76
+
77
+ # --- t-wise sampling --------------------------------------------------------
78
+
79
+ def test_t_wise_sampling_covers_valid_pairs():
80
+ fm = FLAMAFeatureModel(_model(_PLAIN_UVL))
81
+ sample = fm.t_wise_sampling(2, backend="sat")
82
+ selected_sets = [_sel(c) for c in sample]
83
+ # every satisfiable pair is covered; A&B is unsatisfiable (A => !B) so may be absent.
84
+ features = ["A", "B", "C", "D"]
85
+ for f1, f2 in itertools.combinations(features, 2):
86
+ for v1, v2 in itertools.product((True, False), repeat=2):
87
+ covered = any((f1 in s) == v1 and (f2 in s) == v2 for s in selected_sets)
88
+ if not covered:
89
+ assert (f1, f2, v1, v2) == ("A", "B", True, True)
90
+
91
+
92
+ # --- sharpsat backend -------------------------------------------------------
93
+
94
+ def test_sharpsat_backend_count_matches_bdd():
95
+ path = _model(_PLAIN_UVL)
96
+ exact = FLAMAFeatureModel(path).configurations_number(backend="bdd")
97
+ approx = FLAMAFeatureModel(path).configurations_number(backend="sharpsat")
98
+ assert approx == exact
99
+
100
+
101
+ def test_sharpsat_backend_sampling_returns_valid_configurations():
102
+ path = _model(_PLAIN_UVL)
103
+ fm = FLAMAFeatureModel(path)
104
+ sample = fm.sampling(6, with_replacement=True, backend="sharpsat")
105
+ assert len(sample) == 6
106
+ # each sampled configuration is a valid full configuration
107
+ for configuration in sample:
108
+ elements = {f.name: (f.name in _sel(configuration))
109
+ for f in FLAMAFeatureModel(path).fm_model.get_features()}
110
+ assert FLAMAFeatureModel(path).satisfiable_configuration(elements) is True
@@ -1,12 +0,0 @@
1
- flamapy-fw~=2.6.0.dev3
2
- flamapy-fm~=2.6.0.dev3
3
- flamapy-sat~=2.6.0.dev3
4
- flamapy-bdd~=2.6.0.dev3
5
- flamapy-z3~=2.6.0.dev3
6
-
7
- [dev]
8
- pytest
9
- pytest-mock
10
- prospector
11
- mypy
12
- coverage
File without changes
File without changes
File without changes
File without changes