Current section

Files

Jump to
fixpoint test propagation propagator_thread_test.exs
Raw

test/propagation/propagator_thread_test.exs

defmodule CPSolverTest.Propagator.Thread do
use ExUnit.Case
import ExUnit.CaptureLog
import CPSolver.Test.Helpers
describe "Propagator thread" do
alias CPSolver.Propagator.Thread, as: PropagatorThread
alias CPSolver.Store.Registry, as: Store
alias CPSolver.ConstraintStore
alias CPSolver.IntVariable
alias CPSolver.Variable
alias CPSolver.Propagator.NotEqual
@domain_changes CPSolver.Common.domain_changes()
setup do
Logger.configure(level: :debug)
on_exit(fn -> Logger.configure(level: :error) end)
end
@entailment_str "Propagator is entailed"
test "create propagator thread" do
x = 1..1
y = -5..5
variables = Enum.map([x, y], fn d -> IntVariable.new(d) end)
{:ok, [x_var, y_var] = bound_vars, _store} = ConstraintStore.create_store(Store, variables)
{:ok, propagator_thread} =
PropagatorThread.create_thread(self(), {NotEqual, bound_vars},
propagate_on: @domain_changes
)
## Propagator thread subscribes to its variables
assert Enum.all?(bound_vars, fn var -> propagator_thread in Variable.subscribers(var) end)
## ...filters its variables upon start (happens in handle_continue, so needs a small timeout here)
Process.sleep(5)
refute Variable.contains?(y_var, 1)
## ...receives variable update notifications
assert capture_log([level: :debug], fn ->
Variable.removeBelow(y_var, 0)
Process.sleep(10)
end) =~ "Propagation triggered"
## ...triggers filtering on receiving update notifications
assert capture_log([level: :debug], fn ->
Variable.remove(x_var, 1)
Process.sleep(10)
end) =~ "Failure for variable #{inspect(x_var.id)}"
end
test "entailment with initially unfixed variables" do
x = 0..2
y = -5..5
variables = Enum.map([x, y], fn d -> IntVariable.new(d) end)
{:ok, [x_var, y_var] = bound_vars, store} = ConstraintStore.create_store(Store, variables)
{:ok, propagator_thread} = PropagatorThread.create_thread(self(), {NotEqual, bound_vars})
Process.sleep(10)
refute capture_log([level: :debug], fn ->
Store.update(store, x_var, :fix, [1])
Process.sleep(10)
end) =~ @entailment_str
entailment_log =
capture_log([level: :debug], fn ->
Store.update(store, y_var, :fix, [2])
Process.sleep(10)
end)
## An entailment happens exactly once
assert number_of_occurences(entailment_log, @entailment_str) == 1
## Propagator thread discards itself on entailment
Process.sleep(10)
refute Process.alive?(propagator_thread)
end
test "entailment with initially fixed variables" do
x = 0..0
y = 1..1
variables = Enum.map([x, y], fn d -> IntVariable.new(d) end)
{:ok, bound_vars, _store} = ConstraintStore.create_store(Store, variables)
assert capture_log([level: :debug], fn ->
{:ok, propagator_thread} =
PropagatorThread.create_thread(self(), {NotEqual, bound_vars})
## Propagator thread discards itself on entailment
Process.sleep(10)
refute Process.alive?(propagator_thread)
end) =~ @entailment_str
end
test "Starting/stopping propagator subscribes it to/unsubscribes it from its variables" do
x = 0..2
y = -5..5
variables = Enum.map([x, y], fn d -> IntVariable.new(d) end)
{:ok, vars, _store} = ConstraintStore.create_store(Store, variables)
{:ok, propagator_thread} = PropagatorThread.create_thread(self(), {NotEqual, vars})
assert Enum.all?(vars, fn v -> propagator_thread in Variable.subscribers(v) end)
PropagatorThread.dispose(propagator_thread)
Process.sleep(10)
refute Process.alive?(propagator_thread)
refute Enum.any?(vars, fn v -> propagator_thread in Variable.subscribers(v) end)
end
test "stability" do
x = 0..5
y = 1..3
variables = Enum.map([x, y], fn d -> IntVariable.new(d) end)
{:ok, [x_var, y_var] = vars, store} = ConstraintStore.create_store(Store, variables)
## Detects stability on a startup
assert capture_log([level: :debug], fn ->
{:ok, _propagator_thread} =
PropagatorThread.create_thread(self(), {NotEqual, vars})
Process.sleep(10)
end) =~ "is stable"
## Filtering that leaves unfixed variable(s) (x_var in this case) should
## (eventually) put propagator into 'stable' state
assert capture_log([level: :debug], fn ->
Store.update(store, y_var, :fix, [1])
Process.sleep(10)
end) =~ "is stable"
## Fixing all variables (i.e., entailment)
## does not result in stability.
refute capture_log([level: :debug], fn ->
Store.update(store, x_var, :fix, [0])
Process.sleep(10)
end) =~ "is stable"
end
test "propagator failure" do
x = 1..1
y = 1..2
z = 2..2
variables = Enum.map([x, y, z], fn d -> IntVariable.new(d) end)
{:ok, [x_var, y_var, z_var] = _vars, _store} =
ConstraintStore.create_store(Store, variables)
{:ok, _threadXY} =
PropagatorThread.create_thread(self(), {NotEqual, [x_var, y_var]}, id: "X != Y")
{:ok, _threadYZ} =
PropagatorThread.create_thread(self(), {NotEqual, [y_var, z_var]}, id: "Y != Z")
Process.sleep(5)
assert 1 == Variable.min(x_var)
## Non-deterministic failure - fails on either 'y' or 'z', depending on which propagator fixes first.
assert :fail == Variable.min(z_var) || :fail == Variable.min(y_var)
end
end
end