diff --git a/estimage/problems/__init__.py b/estimage/problems/__init__.py index f32beb5..7f6620b 100644 --- a/estimage/problems/__init__.py +++ b/estimage/problems/__init__.py @@ -1,100 +1,2 @@ -import dataclasses -import typing - -import numpy as np - -from ..entities.card import BaseCard -from ..entities.model import EstiModel - - -@dataclasses.dataclass(init=False) -class Problem: - description: str - affected_cards_names: list[str] - tags: set[str] - - def __init__(self): - self.description = "" - self.affected_cards_names = [] - self.tags = frozenset() - - def add_tag(self, tag): - self.tags = frozenset(self.tags.union([tag])) - - -class ProblemDetector: - POINT_THRESHOLD = 0.4 - - def __init__(self, model: EstiModel, cards: typing.Iterable[BaseCard]): - self.model = model - self.cards = cards - self.problems = [] - - self._get_problems() - - def _get_problems(self): - for card in self.cards: - self._get_card_problem(card) - - def _get_card_problem(self, card): - self._analyze_inconsistent_record(card) - - def _numbers_differ_significantly(self, lhs, rhs): - if np.abs(lhs - rhs) > self.POINT_THRESHOLD: - return True - - def _create_and_add_card_problem(self, card): - problem = Problem() - problem.affected_cards_names.append(card.name) - self.problems.append(problem) - return problem - - def _card_is_not_estimated_but_has_children(self, card): - return card.children and card.point_cost == 0 - - def _treat_inconsistent_estimate(self, card, computed_nominal_cost, recorded_cost, expected_computed_cost): - problem = self._create_and_add_card_problem(card) - problem.add_tag("inconsistent_estimate") - if computed_nominal_cost == 0: - self._inconsistent_card_missing_estimates(problem, card, recorded_cost) - else: - self._inconsistent_card_differing_estimate(problem, card, recorded_cost, expected_computed_cost) - - def _card_has_no_children_with_children(self, card): - for child in card.children: - if child.children: - return False - return True - - def _inconsistent_card_missing_estimates(self, problem, card, recorded_cost): - problem.description = ( - f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, " - f"as its children appear to lack estimations." - ) - problem.add_tag("missing_estimates") - if self._card_has_no_children_with_children(card): - problem.add_tag("childless_children") - - def _inconsistent_card_differing_estimate(self, problem, card, recorded_cost, expected_computed_cost): - if expected_computed_cost < recorded_cost: - problem.add_tag("sum_of_children_lower") - problem.description = ( - f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, " - f"while the deduced cost is {expected_computed_cost:.2g}" - ) - - def _analyze_inconsistent_record(self, card): - recorded_cost = card.point_cost - computed_remaining_cost = self.model.remaining_point_estimate_of(card.name).expected - computed_nominal_cost = self.model.nominal_point_estimate_of(card.name).expected - expected_computed_cost = computed_nominal_cost - if card.children: - expected_computed_cost = computed_remaining_cost - - if self._card_is_not_estimated_but_has_children(card): - return - - if not self._numbers_differ_significantly(recorded_cost, expected_computed_cost): - return - - self._treat_inconsistent_estimate(card, computed_nominal_cost, recorded_cost, expected_computed_cost) +from .problem import Problem, ProblemDetector +from .groups import ProblemClassifier diff --git a/estimage/problems/groups.py b/estimage/problems/groups.py new file mode 100644 index 0000000..d021c4a --- /dev/null +++ b/estimage/problems/groups.py @@ -0,0 +1,109 @@ +import collections +import typing + +from .problem import Problem, get_problem +from . import solutions + + +class ProblemCategory: + name: str = "generic" + description: str = "" + solution: solutions.Solution = None + + required_tags: typing.FrozenSet[str] = frozenset() + unwanted_tags: typing.FrozenSet[str] = frozenset() + + def matches(self, p: Problem): + if p.tags.intersection(self.required_tags) != self.required_tags: + return False + if p.tags.intersection(self.unwanted_tags): + return False + return True + + def _get_solution_of(self, p: Problem): + return self.solution(p) + + def get_solution_of(self, p: Problem): + if not self.solution: + return None + return self._get_solution_of(p) + + +class ProblemClassifier: + CATEGORIES: typing.Mapping[str, ProblemCategory] = dict() + classified_problems: typing.Mapping[str, typing.List[Problem]] + _problems = typing.Mapping[Problem, str] + + def __init__(self): + self.not_classified = [] + self.CATEGORIES = dict(** self.CATEGORIES) + self.classified_problems = collections.defaultdict(list) + self._problems = dict() + + def classify(self, problems: typing.Iterable[Problem]): + for p in problems: + self._classify_problem(p) + + def _classify_problem(self, problem: Problem): + for c_name, c in self.CATEGORIES.items(): + if c.matches(problem): + self.classified_problems[c_name].append(problem) + self._problems[problem] = c_name + return + self.not_classified.append(problem) + + def get_category_of(self, problem: Problem): + cat_name = self._problems.get(problem, None) + return self.CATEGORIES.get(cat_name, ProblemCategory()) + + def get_categories_with_problems(self): + return [self.CATEGORIES[name] for name in self.classified_problems] + + def add_category(self, cat_type: typing.Type[ProblemCategory]): + if (name := cat_type.name) in self.CATEGORIES: + msg = f"Already have a category named '{name}'" + raise KeyError(msg) + self.CATEGORIES[name] = cat_type() + + +def problem_category(cls): + ProblemClassifier.CATEGORIES[cls.name] = cls() + return cls + + +@problem_category +class ReasonableOutdated(ProblemCategory): + name = "reasonable_outdated" + description = "Estimate is inconsistent with children, but lower than the nominal size and greater than the size of tasks not yet completed." + solution = solutions.SolutionByUpdatingSelf + + required_tags = frozenset([ + "inconsistent_estimate", + "estimate_within_nominal", + "sum_of_children_lower", + ]) + unwanted_tags = frozenset([ + "missing_estimates", + ]) + + +@problem_category +class GenericInconsistent(ProblemCategory): + name = "generic_inconsistent" + solution = solutions.SolutionByUpdatingSelf + + required_tags = frozenset(["inconsistent_estimate"]) + unwanted_tags = frozenset(["missing_estimates"]) + + +@problem_category +class UnestimatedChildren(ProblemCategory): + name = "unestimated_children" + description = "Children have no size estimated, but the parent issue has." + solution = solutions.SolutionByUpdatingChildren + + required_tags = set([ + "inconsistent_estimate", + "missing_estimates", + "childless_children", + ]) diff --git a/estimage/problems/problem.py b/estimage/problems/problem.py new file mode 100644 index 0000000..aef6fdd --- /dev/null +++ b/estimage/problems/problem.py @@ -0,0 +1,129 @@ +import dataclasses +import typing +import collections + +import numpy as np + +from ..entities.card import BaseCard +from ..entities.model import EstiModel + + +TAG_TO_PROBLEM_TYPE = dict() + +def get_problem(** data): + data["tags"] = frozenset(data.get("tags", frozenset())) + problem_tags = list(data["tags"].intersection(set(TAG_TO_PROBLEM_TYPE.keys()))) + if not problem_tags: + return Problem(** data) + return TAG_TO_PROBLEM_TYPE[problem_tags[0]](** data) + + +def problem_associated_with_tag(tag): + def wrapper(wrapped): + TAG_TO_PROBLEM_TYPE[tag] = wrapped + return wrapped + return wrapper + + +@dataclasses.dataclass(frozen=True) +class Problem: + description: str = "" + affected_card_name: str = "" + tags: typing.FrozenSet[str] = frozenset() + + def format_task_name(self): + return self.affected_card_name + + +@problem_associated_with_tag("inconsistent_estimate") +@dataclasses.dataclass(frozen=True) +class ValueProblem(Problem): + value_expected: float = None + value_found: float = None + + +class ProblemDetector: + POINT_THRESHOLD = 0.4 + + def __init__(self, model: EstiModel, cards: typing.Iterable[BaseCard]): + self.model = model + self.cards = cards + self.problems = [] + + self._get_problems() + + def _get_problems(self): + for card in self.cards: + self._get_card_problem(card) + + def _get_card_problem(self, card): + self._analyze_inconsistent_record(card) + + def _numbers_differ_significantly(self, lhs, rhs): + if np.abs(lhs - rhs) > self.POINT_THRESHOLD: + return True + + def _create_card_problem_data(self, card): + data = dict() + data["affected_card_name"] = card.name + data["tags"] = set() + return data + + def _card_is_not_estimated_but_has_children(self, card): + return card.children and card.point_cost == 0 + + def _treat_inconsistent_estimate(self, card, computed_nominal_cost, recorded_cost, expected_computed_cost): + data = self._create_card_problem_data(card) + data["tags"].add("inconsistent_estimate") + data["value_found"] = recorded_cost + if computed_nominal_cost == 0: + self._inconsistent_card_missing_children_estimates(data, card) + else: + self._inconsistent_card_differing_estimate(data, card, expected_computed_cost, computed_nominal_cost) + data["tags"] = frozenset(data["tags"]) + self.problems.append(get_problem(** data)) + + def _card_has_no_children_with_children(self, card): + for child in card.children: + if child.children: + return False + return True + + def _inconsistent_card_missing_children_estimates(self, data, card): + recorded_cost = data['value_found'] + data["description"] = ( + f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, " + f"as its children appear to lack estimations." + ) + data["tags"].add("missing_children_estimates") + if self._card_has_no_children_with_children(card): + data["tags"].add("has_only_childless_children") + + def _inconsistent_card_differing_estimate(self, data, card, expected_computed_cost, computed_nominal_cost): + recorded_cost = data['value_found'] + if expected_computed_cost < recorded_cost: + data["tags"].add("sum_of_children_lower") + if recorded_cost <= computed_nominal_cost: + data["tags"].add("estimate_within_nominal") + data["description"] = ( + f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, " + f"while the deduced cost is {expected_computed_cost:.2g}" + ) + data["value_expected"] = expected_computed_cost + + def _analyze_inconsistent_record(self, card): + recorded_cost = card.point_cost + computed_remaining_cost = self.model.remaining_point_estimate_of(card.name).expected + computed_nominal_cost = self.model.nominal_point_estimate_of(card.name).expected + expected_computed_cost = computed_nominal_cost + if card.children: + expected_computed_cost = computed_remaining_cost + + if self._card_is_not_estimated_but_has_children(card): + return + + if not self._numbers_differ_significantly(recorded_cost, expected_computed_cost): + return + + self._treat_inconsistent_estimate(card, computed_nominal_cost, recorded_cost, expected_computed_cost) + diff --git a/estimage/problems/solutions.py b/estimage/problems/solutions.py new file mode 100644 index 0000000..1bd0215 --- /dev/null +++ b/estimage/problems/solutions.py @@ -0,0 +1,47 @@ +import typing + +from ..entities.card import BaseCard +from .problem import Problem + + +class Solution: + card_name: str + description: str = "" + + def __init__(self, problem: Problem): + self.problem = problem + + def describe(self): + return "" + + +class SolutionByUpdating(Solution): + updates_model: bool + + def __init__(self, problem: Problem): + super().__init__(problem) + self.updates_model = True + + def describe(self): + return f"Update the record of '{self.card_name}'" + + +class SolutionByUpdatingChildren(SolutionByUpdating): + action = "update_children_points" + description = "Update children of respective card, so the subtree is consistent" + + def describe(self): + return f"Update children of '{self.card_name}', so they become consistent with the its record." + + +class SolutionByUpdatingSelf(SolutionByUpdating): + action = "update_points" + description = "Update the respective card, so it is consistent with its children" + value: float + + def __init__(self, problem: Problem): + super().__init__(problem) + self.updates_model = False + + def describe(self): + return f"Update the record of '{self.card_name}', so it matches records of its children." diff --git a/estimage/solutions/__init__.py b/estimage/solutions/__init__.py deleted file mode 100644 index fd12f56..0000000 --- a/estimage/solutions/__init__.py +++ /dev/null @@ -1,102 +0,0 @@ -import dataclasses -import typing - -from ..entities.card import BaseCard -from ..problems import Problem - - -@dataclasses.dataclass(init=False) -class Solution: - card_name: str - - def __init__(self): - self.card_name = "" - - def describe(self): - return "" - - def prime(self, cards: typing.Iterable[BaseCard]): - raise NotImplementedError() - - -class SolutionByUpdating(Solution): - end_value: float - - def __init__(self): - super().__init__() - self.end_value = None - - def describe(self): - return f"Update the record of '{self.card_name}'" - -class SolutionByUpdatingChildren(SolutionByUpdating): - action = "update_children_points" - - def describe(self): - return f"Update children of '{self.card_name}', so they become consistent with the its record." - - def prime(self, cards: typing.Iterable[BaseCard]): - my_card = [c for c in cards if c.name == self.card_name][0] - self.end_value = my_card.point_cost / len(my_card.children) - pass - - -class SolutionByUpdatingSelf(SolutionByUpdating): - action = "update_points" - - def __init__(self): - super().__init__() - - def describe(self): - return f"Update the record of '{self.card_name}', so it matches records of its children." - - def prime(self, cards: typing.Iterable[BaseCard]): - pass - - -class ProblemSolver: - SOLUTIONS = [] - - def get_solutions(self, problems: typing.Iterable[Problem]): - solutions = [] - for problem in problems: - solution = self.get_solution_of(problem) - if solution: - solutions.append(solution) - return solutions - - def get_solution_of(self, problem: Problem): - for solution in self.SOLUTIONS: - ret = solution(problem) - if ret: - return ret - - -def problem_solution(func): - ProblemSolver.SOLUTIONS.append(func) - return func - - -@problem_solution -def get_solution_of_inconsistent_parent(problem: Problem): - if "inconsistent_estimate" not in problem.tags: - return - if "missing_estimates" in problem.tags: - return - ret = SolutionByUpdatingSelf() - ret.card_name = problem.affected_cards_names[0] - return ret - - -@problem_solution -def get_solution_of_inconsistent_children(problem: Problem): - if "inconsistent_estimate" not in problem.tags: - return - if "missing_estimates" not in problem.tags: - return - if "childless_children" not in problem.tags: - return - - ret = SolutionByUpdatingChildren() - ret.card_name = problem.affected_cards_names[0] - return ret diff --git a/estimage/webapp/main/forms.py b/estimage/webapp/main/forms.py index e8118e8..b540780 100644 --- a/estimage/webapp/main/forms.py +++ b/estimage/webapp/main/forms.py @@ -98,3 +98,34 @@ class PointEstimationForm(FlaskForm): most_likely = wtforms.SelectField("Most Likely", choices=FIB) pessimistic = wtforms.SelectField("Pessimistic", choices=FIB) submit = SubmitField("Save Estimate") + + +class MultiCheckboxField(wtforms.SelectMultipleField): + """ + A multiple-select, except displays a list of checkboxes. + + Iterating the field will produce subfields, allowing custom rendering of + the enclosed checkbox fields. + """ + widget = wtforms.widgets.ListWidget(prefix_label=False) + option_widget = wtforms.widgets.CheckboxInput() + + def get_selected(self): + ret = [] + for x in self: + print(11, x, 22) + + +class ProblemForm(FlaskForm): + + def add_problems(self, problems_category, problems): + for p in problems: + self.problems.choices.append((p.affected_card_name, p.description)) + + if s := problems_category.solution: + self.solution.data = s.description + + problem = wtforms.HiddenField("problem") + problems = MultiCheckboxField("Problems", choices=[]) + solution = wtforms.StringField("Solution", render_kw={'readonly': True}) + submit = SubmitField("Solve Problems") diff --git a/estimage/webapp/main/routes.py b/estimage/webapp/main/routes.py index 16051fb..2a6b8a3 100644 --- a/estimage/webapp/main/routes.py +++ b/estimage/webapp/main/routes.py @@ -443,10 +443,46 @@ def view_problems(): all_cards = list(all_cards_by_id.values()) problem_detector = problems.ProblemDetector(model, all_cards) - probs = problem_detector.problems - solver = solutions.ProblemSolver() - sols = {p.description: solver.get_solution_of(p) for p in probs} + + classifier = problems.groups.ProblemClassifier() + classifier.classify(problem_detector.problems) + categories = classifier.get_categories_with_problems() + + cat_forms = [] + for cat in categories: + probs = classifier.classified_problems[cat.name] + + form = forms.ProblemForm() + form.add_problems(cat, probs) + form.problem.data = "Missing Update" + + cat_forms.append((cat, form)) + + # solver = solutions.ProblemSolver() + # sols = {p.description: solver.get_solution_of(p) for p in probs} return web_utils.render_template( 'problems.html', title='Problems', - all_cards_by_id=all_cards_by_id, problems=probs, solutions=sols) + all_cards_by_id=all_cards_by_id, problems=probs, catforms=cat_forms) + + +@bp.route('/problems/fix', methods=['POST']) +@flask_login.login_required +def fix_problems(): + user = flask_login.current_user + user_id = user.get_id() + + all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id) + all_cards = list(all_cards_by_id.values()) + + problem_detector = problems.ProblemDetector(model, all_cards) + + form = forms.ProblemForm() + form.add_problems(problem_detector.problems) + if form.validate_on_submit(): + print(f"Fix {form.problem.data} by {form.solution.data}") + print(f"Fix: {form.problems.data}") + else: + flask.flash(f"Error handing over solution: {form.errors}") + return flask.redirect( + web_utils.head_url_for("main.view_problems")) diff --git a/estimage/webapp/templates/problems.html b/estimage/webapp/templates/problems.html index 737300f..f933506 100644 --- a/estimage/webapp/templates/problems.html +++ b/estimage/webapp/templates/problems.html @@ -1,14 +1,11 @@ {% extends "general_retro.html" %} {% import "utils.j2" as utils with context %} +{% from 'bootstrap5/form.html' import render_form %} -{% macro print_name_replaced_with_links(text, names) %} -{% if names | length > 1 %} -{{ print_name_replaced_with_links(print_name_replaced_with_links(text, [names[0]]), names[1:]) }} -{% else %} -{{ text.replace(names[0], utils.task_or_epic_link(all_cards_by_id[names[0]], "retrospective")) | safe }} -{% endif %} +{% macro print_name_replaced_with_links(text, name) %} +{{ text.replace(name, utils.task_or_epic_link(all_cards_by_id[name], "retrospective")) | safe }} {% endmacro %} @@ -16,24 +13,13 @@

Problems

+ {% for cat, form in catforms %}
- {% for p in problems %} -

{{ " ".join(p.affected_cards_names) }}

-

- {{ print_name_replaced_with_links(p.description, p.affected_cards_names) }} -

-

- Tags: -

    - {% for t in p.tags %} -
  • {{ t }}
  • - {% endfor %} -
- Solution: - {{ solutions[p.description].describe() }} -

- {% endfor %} +

{{ cat.name }}

+

{{ cat.description }}

+ {{ render_form(form, action=head_url_for("main.fix_problems")) }}
+ {% endfor %}
{% endblock content %} diff --git a/tests/test_problems.py b/tests/test_problems.py index 3a29b97..e9e6453 100644 --- a/tests/test_problems.py +++ b/tests/test_problems.py @@ -1,6 +1,6 @@ import pytest -import estimage.problems as tm +import estimage.problems.problem as tm @pytest.fixture @@ -62,16 +62,33 @@ def test_model_notices_basic_inconsistency(cards_one_two): assert "inconsistent_estimate" in problem.tags assert "sum_of_children_lower" not in problem.tags assert "one" in problem.description - assert "is 2" in problem.description + assert problem.affected_card_name == "one" + assert problem.value_expected == 2 + assert problem.value_found == 1 + assert "2" in problem.description assert "of 1" in problem.description -def test_model_notices_inconsistency_maybe_caused_by_progress(cards_one_two): +def test_model_notices_inconsistency_unlikely_caused_by_progress(cards_one_two): card_one, card_two = cards_one_two card_two.point_cost = 0.5 problem = get_problem_of_cards(cards_one_two) assert "inconsistent_estimate" in problem.tags assert "sum_of_children_lower" in problem.tags + assert not "estimate_within_nominal" in problem.tags + assert problem.value_expected == 0.5 + assert problem.value_found == 1 + + +def test_model_notices_inconsistency_probably_caused_by_progress(cards_one_two): + card_one, card_two = cards_one_two + card_two.status = "done" + problem = get_problem_of_cards(cards_one_two) + assert "inconsistent_estimate" in problem.tags + assert "sum_of_children_lower" in problem.tags + assert "estimate_within_nominal" in problem.tags + assert problem.value_expected == 0 + assert problem.value_found == 1 def test_model_notices_children_not_estimated(cards_one_two): @@ -80,15 +97,19 @@ def test_model_notices_children_not_estimated(cards_one_two): card_two.status = "done" problem = get_problem_of_cards(cards_one_two) assert "inconsistent_estimate" in problem.tags - assert "missing_estimates" not in problem.tags + assert "missing_children_estimates" not in problem.tags assert "one" in problem.description assert "not estimated" not in problem.description + assert problem.value_expected == 0 + assert problem.value_found == 1 card_two.point_cost = 0 problem = get_problem_of_cards(cards_one_two) assert "one" in problem.description assert "inconsistent_estimate" in problem.tags - assert "missing_estimates" in problem.tags + assert "missing_children_estimates" in problem.tags + assert problem.value_expected is None + assert problem.value_found == 1 def test_model_finds_status_problem(cards_one_two): @@ -99,4 +120,4 @@ def test_model_finds_status_problem(cards_one_two): card_two.point_cost = card_one.point_cost problem = get_problem_of_cards(cards_one_two) - assert "one" in problem.affected_cards_names + assert "one" == problem.affected_card_name diff --git a/tests/test_problems_groups.py b/tests/test_problems_groups.py new file mode 100644 index 0000000..05638c3 --- /dev/null +++ b/tests/test_problems_groups.py @@ -0,0 +1,74 @@ +import pytest + +import estimage.problems.groups as tm + +import test_problems +from test_problems import cards_one_two + + +def test_problem_categories_trivial(): + dumb_classifier = tm.ProblemClassifier() + p = tm.get_problem(tags=["unspecific"]) + dumb_classifier.classify([p]) + others = dumb_classifier.not_classified + assert len(others) == 1 + assert others[0] == p + + +class CustomClassifier(tm.ProblemClassifier): + pass + + +@pytest.fixture +def classifier(): + return CustomClassifier() + + +class Underestimation(tm.ProblemCategory): + name = "underestimation" + + def matches(self, p): + return "underestimated" in p.tags + + +def test_problem_category_match(): + cat = Underestimation() + p = tm.Problem(tags=["underestimated"]) + assert cat.matches(p) + + bad_p = tm.Problem(tags=["nothing"]) + assert not cat.matches(bad_p) + + +def test_problem_categories_no_duplication(classifier): + classifier.add_category(Underestimation) + with pytest.raises(KeyError): + classifier.add_category(Underestimation) + + +def test_problem_categories_basic(classifier): + classifier.add_category(Underestimation) + p = tm.get_problem(tags=["underestimated"]) + classifier.classify([p]) + assert not classifier.not_classified + underestimation_problems = classifier.classified_problems["underestimation"] + assert underestimation_problems[0] == p + + +def test_basic_inconsistency_solution(classifier, cards_one_two): + card_one, card_two = cards_one_two + card_two.point_cost = 2 + + problem = test_problems.get_problem_of_cards(cards_one_two) + classifier.classify([problem]) + problem_category = classifier.get_category_of(problem) + assert problem_category.name == "generic_inconsistent" + solution_type = problem_category.solution + assert solution_type.action == "update_points" + assert "Update the respective card" in solution_type.description + solution = problem_category.get_solution_of(problem) + assert solution.problem.affected_card_name == "one" + assert solution.updates_model == False + + only_category = classifier.get_categories_with_problems()[0] + assert only_category.name == problem_category.name diff --git a/tests/test_solutions.py b/tests/test_solutions.py deleted file mode 100644 index 71d127b..0000000 --- a/tests/test_solutions.py +++ /dev/null @@ -1,56 +0,0 @@ -import pytest - -import estimage.solutions as tm -import estimage.problems as problems - -import test_problems as ut -from test_problems import cards_one_two - - -@pytest.fixture -def solver(): - return tm.ProblemSolver() - - -def test_no_problem_no_solution(solver): - assert len(solver.get_solutions([])) == 0 - - -def test_basic_inconsistency_solution(solver, cards_one_two): - card_one, card_two = cards_one_two - card_two.point_cost = 2 - - problem = ut.get_problem_of_cards(cards_one_two) - solutions = solver.get_solutions([problem]) - assert len(solutions) == 1 - solution = solutions[0] - assert solution.action == "update_points" - assert solution.card_name == "one" - solution.prime([card_one]) - # assert solution.end_value == card_two.point_cost - - -def test_update_children_inconsistency_solution(solver, cards_one_two): - card_one, card_two = cards_one_two - card_two.point_cost = 0 - card_three = tm.BaseCard("three") - card_one.add_element(card_three) - - problem = ut.get_problem_of_cards(cards_one_two) - solutions = solver.get_solutions([problem]) - assert len(solutions) == 1 - solution = solutions[0] - assert solution.action == "update_children_points" - assert solution.card_name == "one" - solution.prime([card_one]) - assert solution.end_value == card_one.point_cost / 2.0 - - -def test_update_complex_children_no_solution(solver, cards_one_two): - card_one, card_two = cards_one_two - card_three = problems.BaseCard("three") - card_two.add_element(card_three) - - problem = ut.get_problem_of_cards([card_one]) - solutions = solver.get_solutions([problem]) - assert len(solutions) == 0