feat(conflicts): added conflicts business and presentation logic
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
from apps.conflicts.models import ConflictPolicy
|
||||
from apps.conflicts.services import conflict_domain_create
|
||||
|
||||
|
||||
def make_domain(
|
||||
suffix="",
|
||||
policy=ConflictPolicy.MUTUAL_EXCLUSION,
|
||||
max_concurrent=1,
|
||||
):
|
||||
return conflict_domain_create(
|
||||
name=f"domain{suffix}",
|
||||
description=f"Test domain {suffix}",
|
||||
policy=policy,
|
||||
max_concurrent=max_concurrent,
|
||||
)
|
||||
@@ -0,0 +1,337 @@
|
||||
from typing import override
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.conflicts.models import (
|
||||
ConflictDomain,
|
||||
ConflictPolicy,
|
||||
ExperimentConflictDomain,
|
||||
)
|
||||
from apps.conflicts.selectors import (
|
||||
conflict_domain_get,
|
||||
conflict_domain_list,
|
||||
domain_active_experiments,
|
||||
experiment_conflict_domains,
|
||||
)
|
||||
from apps.conflicts.services import (
|
||||
conflict_domain_create,
|
||||
conflict_domain_delete,
|
||||
conflict_domain_update,
|
||||
experiment_add_to_domain,
|
||||
experiment_remove_from_domain,
|
||||
experiment_update_domain_priority,
|
||||
resolve_domain_conflict,
|
||||
validate_domain_conflicts,
|
||||
)
|
||||
from apps.conflicts.tests.helpers import make_domain
|
||||
from apps.experiments.models import ExperimentStatus
|
||||
from apps.experiments.services import (
|
||||
experiment_approve,
|
||||
experiment_create,
|
||||
experiment_start,
|
||||
experiment_submit_for_review,
|
||||
)
|
||||
from apps.experiments.tests.helpers import add_two_variants, make_flag
|
||||
from apps.reviews.services import review_settings_update
|
||||
from apps.reviews.tests.helpers import make_approver, make_experimenter
|
||||
|
||||
|
||||
class ConflictDomainCRUDTest(TestCase):
|
||||
def test_creates_domain(self) -> None:
|
||||
domain = make_domain(suffix="_c1")
|
||||
self.assertEqual(domain.name, "domain_c1")
|
||||
self.assertEqual(domain.policy, ConflictPolicy.MUTUAL_EXCLUSION)
|
||||
self.assertEqual(domain.max_concurrent, 1)
|
||||
|
||||
def test_creates_domain_with_priority_policy(self) -> None:
|
||||
domain = make_domain(
|
||||
suffix="_c2",
|
||||
policy=ConflictPolicy.PRIORITY,
|
||||
max_concurrent=3,
|
||||
)
|
||||
self.assertEqual(domain.policy, ConflictPolicy.PRIORITY)
|
||||
self.assertEqual(domain.max_concurrent, 3)
|
||||
|
||||
def test_duplicate_name_fails(self) -> None:
|
||||
make_domain(suffix="_dup")
|
||||
with self.assertRaises(Exception):
|
||||
make_domain(suffix="_dup")
|
||||
|
||||
def test_updates_domain(self) -> None:
|
||||
domain = make_domain(suffix="_upd")
|
||||
domain = conflict_domain_update(
|
||||
domain=domain,
|
||||
description="Updated",
|
||||
max_concurrent=5,
|
||||
)
|
||||
domain.refresh_from_db()
|
||||
self.assertEqual(domain.description, "Updated")
|
||||
self.assertEqual(domain.max_concurrent, 5)
|
||||
|
||||
def test_deletes_domain_without_active(self) -> None:
|
||||
domain = make_domain(suffix="_del")
|
||||
pk = domain.pk
|
||||
conflict_domain_delete(domain=domain)
|
||||
self.assertFalse(ConflictDomain.objects.filter(pk=pk).exists())
|
||||
|
||||
def test_list_domains(self) -> None:
|
||||
make_domain(suffix="_l1")
|
||||
make_domain(suffix="_l2")
|
||||
domains = conflict_domain_list()
|
||||
names = [d.name for d in domains]
|
||||
self.assertIn("domain_l1", names)
|
||||
self.assertIn("domain_l2", names)
|
||||
|
||||
def test_get_domain(self) -> None:
|
||||
domain = make_domain(suffix="_get")
|
||||
fetched = conflict_domain_get(domain.pk)
|
||||
self.assertEqual(fetched.pk, domain.pk)
|
||||
|
||||
|
||||
class ExperimentDomainMembershipTest(TestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
self.experimenter = make_experimenter("_cdm")
|
||||
self.approver = make_approver("_cdm")
|
||||
self.flag = make_flag(suffix="_cdm")
|
||||
review_settings_update(
|
||||
default_min_approvals=1, allow_any_approver=True
|
||||
)
|
||||
self.domain = make_domain(suffix="_cdm")
|
||||
|
||||
def _make_ready_experiment(self, suffix=""):
|
||||
exp = experiment_create(
|
||||
flag=make_flag(suffix=f"_cdm{suffix}"),
|
||||
name=f"CdmExp{suffix}",
|
||||
owner=self.experimenter,
|
||||
)
|
||||
add_two_variants(exp)
|
||||
return exp
|
||||
|
||||
def _approve(self, exp):
|
||||
exp = experiment_submit_for_review(
|
||||
experiment=exp, user=self.experimenter
|
||||
)
|
||||
return experiment_approve(experiment=exp, approver=self.approver)
|
||||
|
||||
def test_add_experiment_to_domain(self) -> None:
|
||||
exp = self._make_ready_experiment("_add")
|
||||
membership = experiment_add_to_domain(
|
||||
experiment=exp,
|
||||
domain=self.domain,
|
||||
priority=10,
|
||||
)
|
||||
self.assertEqual(membership.priority, 10)
|
||||
self.assertEqual(membership.conflict_domain, self.domain)
|
||||
|
||||
def test_duplicate_membership_fails(self) -> None:
|
||||
exp = self._make_ready_experiment("_dup2")
|
||||
experiment_add_to_domain(experiment=exp, domain=self.domain)
|
||||
with self.assertRaises(Exception):
|
||||
experiment_add_to_domain(experiment=exp, domain=self.domain)
|
||||
|
||||
def test_remove_experiment_from_domain(self) -> None:
|
||||
exp = self._make_ready_experiment("_rm")
|
||||
experiment_add_to_domain(experiment=exp, domain=self.domain)
|
||||
experiment_remove_from_domain(experiment=exp, domain=self.domain)
|
||||
self.assertFalse(
|
||||
ExperimentConflictDomain.objects.filter(
|
||||
experiment=exp, conflict_domain=self.domain
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_remove_nonexistent_fails(self) -> None:
|
||||
exp = self._make_ready_experiment("_rmne")
|
||||
with self.assertRaises(ValidationError):
|
||||
experiment_remove_from_domain(experiment=exp, domain=self.domain)
|
||||
|
||||
def test_update_priority(self) -> None:
|
||||
exp = self._make_ready_experiment("_pri")
|
||||
experiment_add_to_domain(
|
||||
experiment=exp, domain=self.domain, priority=1
|
||||
)
|
||||
membership = experiment_update_domain_priority(
|
||||
experiment=exp, domain=self.domain, priority=99
|
||||
)
|
||||
self.assertEqual(membership.priority, 99)
|
||||
|
||||
def test_experiment_conflict_domains_selector(self) -> None:
|
||||
exp = self._make_ready_experiment("_sel")
|
||||
domain2 = make_domain(suffix="_sel2")
|
||||
experiment_add_to_domain(experiment=exp, domain=self.domain)
|
||||
experiment_add_to_domain(experiment=exp, domain=domain2)
|
||||
memberships = experiment_conflict_domains(exp.pk)
|
||||
self.assertEqual(memberships.count(), 2)
|
||||
|
||||
def test_domain_active_experiments_selector(self) -> None:
|
||||
exp = self._make_ready_experiment("_ae")
|
||||
experiment_add_to_domain(experiment=exp, domain=self.domain)
|
||||
exp = self._approve(exp)
|
||||
exp = experiment_start(experiment=exp, user=self.experimenter)
|
||||
active = domain_active_experiments(self.domain.pk)
|
||||
self.assertEqual(active.count(), 1)
|
||||
self.assertEqual(active.first().experiment.pk, exp.pk)
|
||||
|
||||
def test_delete_domain_with_active_experiment_fails(self) -> None:
|
||||
exp = self._make_ready_experiment("_daf")
|
||||
experiment_add_to_domain(experiment=exp, domain=self.domain)
|
||||
exp = self._approve(exp)
|
||||
experiment_start(experiment=exp, user=self.experimenter)
|
||||
with self.assertRaises(ValidationError):
|
||||
conflict_domain_delete(domain=self.domain)
|
||||
|
||||
|
||||
class DomainConflictValidationTest(TestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
self.experimenter = make_experimenter("_dcv")
|
||||
self.approver = make_approver("_dcv")
|
||||
review_settings_update(
|
||||
default_min_approvals=1, allow_any_approver=True
|
||||
)
|
||||
self.domain = make_domain(suffix="_dcv", max_concurrent=1)
|
||||
|
||||
def _make_and_start(self, suffix):
|
||||
flag = make_flag(suffix=f"_dcv{suffix}")
|
||||
exp = experiment_create(
|
||||
flag=flag, name=f"DcvExp{suffix}", owner=self.experimenter
|
||||
)
|
||||
add_two_variants(exp)
|
||||
experiment_add_to_domain(experiment=exp, domain=self.domain)
|
||||
exp = experiment_submit_for_review(
|
||||
experiment=exp, user=self.experimenter
|
||||
)
|
||||
exp = experiment_approve(experiment=exp, approver=self.approver)
|
||||
return experiment_start(experiment=exp, user=self.experimenter)
|
||||
|
||||
def test_start_blocked_by_domain_conflict(self) -> None:
|
||||
self._make_and_start("_1")
|
||||
flag2 = make_flag(suffix="_dcv_2b")
|
||||
exp2 = experiment_create(
|
||||
flag=flag2, name="DcvExp2B", owner=self.experimenter
|
||||
)
|
||||
add_two_variants(exp2)
|
||||
experiment_add_to_domain(experiment=exp2, domain=self.domain)
|
||||
exp2 = experiment_submit_for_review(
|
||||
experiment=exp2, user=self.experimenter
|
||||
)
|
||||
exp2 = experiment_approve(experiment=exp2, approver=self.approver)
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
experiment_start(experiment=exp2, user=self.experimenter)
|
||||
self.assertIn("conflict_domain", str(ctx.exception))
|
||||
|
||||
def test_start_allowed_when_domain_has_capacity(self) -> None:
|
||||
domain = make_domain(suffix="_cap", max_concurrent=2)
|
||||
flag1 = make_flag(suffix="_cap1")
|
||||
exp1 = experiment_create(
|
||||
flag=flag1, name="CapExp1", owner=self.experimenter
|
||||
)
|
||||
add_two_variants(exp1)
|
||||
experiment_add_to_domain(experiment=exp1, domain=domain)
|
||||
exp1 = experiment_submit_for_review(
|
||||
experiment=exp1, user=self.experimenter
|
||||
)
|
||||
exp1 = experiment_approve(experiment=exp1, approver=self.approver)
|
||||
experiment_start(experiment=exp1, user=self.experimenter)
|
||||
|
||||
flag2 = make_flag(suffix="_cap2")
|
||||
exp2 = experiment_create(
|
||||
flag=flag2, name="CapExp2", owner=self.experimenter
|
||||
)
|
||||
add_two_variants(exp2)
|
||||
experiment_add_to_domain(experiment=exp2, domain=domain)
|
||||
exp2 = experiment_submit_for_review(
|
||||
experiment=exp2, user=self.experimenter
|
||||
)
|
||||
exp2 = experiment_approve(experiment=exp2, approver=self.approver)
|
||||
exp2 = experiment_start(experiment=exp2, user=self.experimenter)
|
||||
self.assertEqual(exp2.status, ExperimentStatus.RUNNING)
|
||||
|
||||
def test_no_domain_no_conflict(self) -> None:
|
||||
flag = make_flag(suffix="_nodom")
|
||||
exp = experiment_create(
|
||||
flag=flag, name="NoDomExp", owner=self.experimenter
|
||||
)
|
||||
add_two_variants(exp)
|
||||
exp = experiment_submit_for_review(
|
||||
experiment=exp, user=self.experimenter
|
||||
)
|
||||
exp = experiment_approve(experiment=exp, approver=self.approver)
|
||||
exp = experiment_start(experiment=exp, user=self.experimenter)
|
||||
self.assertEqual(exp.status, ExperimentStatus.RUNNING)
|
||||
|
||||
|
||||
class ResolveDomainConflictTest(TestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
self.experimenter = make_experimenter("_rdc")
|
||||
self.approver = make_approver("_rdc")
|
||||
review_settings_update(
|
||||
default_min_approvals=1, allow_any_approver=True
|
||||
)
|
||||
|
||||
def _make_and_start(self, suffix, domain, priority=0):
|
||||
flag = make_flag(suffix=f"_rdc{suffix}")
|
||||
exp = experiment_create(
|
||||
flag=flag, name=f"RdcExp{suffix}", owner=self.experimenter
|
||||
)
|
||||
add_two_variants(exp)
|
||||
experiment_add_to_domain(
|
||||
experiment=exp, domain=domain, priority=priority
|
||||
)
|
||||
exp = experiment_submit_for_review(
|
||||
experiment=exp, user=self.experimenter
|
||||
)
|
||||
exp = experiment_approve(experiment=exp, approver=self.approver)
|
||||
return experiment_start(experiment=exp, user=self.experimenter)
|
||||
|
||||
def test_mutual_exclusion_winner_is_first(self) -> None:
|
||||
domain = make_domain(
|
||||
suffix="_me",
|
||||
policy=ConflictPolicy.MUTUAL_EXCLUSION,
|
||||
max_concurrent=3,
|
||||
)
|
||||
exp1 = self._make_and_start("_me1", domain)
|
||||
exp2 = self._make_and_start("_me2", domain)
|
||||
winner = resolve_domain_conflict(exp1.pk, domain.pk, "u1")
|
||||
self.assertTrue(winner)
|
||||
loser = resolve_domain_conflict(exp2.pk, domain.pk, "u1")
|
||||
self.assertFalse(loser)
|
||||
|
||||
def test_priority_higher_wins(self) -> None:
|
||||
domain = make_domain(
|
||||
suffix="_pr",
|
||||
policy=ConflictPolicy.PRIORITY,
|
||||
max_concurrent=3,
|
||||
)
|
||||
exp_low = self._make_and_start("_pr1", domain, priority=1)
|
||||
exp_high = self._make_and_start("_pr2", domain, priority=10)
|
||||
self.assertTrue(
|
||||
resolve_domain_conflict(exp_high.pk, domain.pk, "u1")
|
||||
)
|
||||
self.assertFalse(
|
||||
resolve_domain_conflict(exp_low.pk, domain.pk, "u1")
|
||||
)
|
||||
|
||||
def test_priority_tie_first_created_wins(self) -> None:
|
||||
domain = make_domain(
|
||||
suffix="_tie",
|
||||
policy=ConflictPolicy.PRIORITY,
|
||||
max_concurrent=3,
|
||||
)
|
||||
exp1 = self._make_and_start("_tie1", domain, priority=5)
|
||||
exp2 = self._make_and_start("_tie2", domain, priority=5)
|
||||
self.assertTrue(
|
||||
resolve_domain_conflict(exp1.pk, domain.pk, "u1")
|
||||
)
|
||||
self.assertFalse(
|
||||
resolve_domain_conflict(exp2.pk, domain.pk, "u1")
|
||||
)
|
||||
|
||||
def test_single_experiment_always_wins(self) -> None:
|
||||
domain = make_domain(suffix="_single")
|
||||
exp = self._make_and_start("_s", domain)
|
||||
self.assertTrue(
|
||||
resolve_domain_conflict(exp.pk, domain.pk, "u1")
|
||||
)
|
||||
Reference in New Issue
Block a user