317 lines
9.1 KiB
Elixir
317 lines
9.1 KiB
Elixir
|
defmodule Ecto.Integration.SandboxTest do
|
||
|
use ExUnit.Case
|
||
|
|
||
|
alias Ecto.Adapters.SQL.Sandbox
|
||
|
alias Ecto.Integration.{PoolRepo, TestRepo}
|
||
|
alias Ecto.Integration.Post
|
||
|
|
||
|
import ExUnit.CaptureLog
|
||
|
|
||
|
Application.put_env(:ecto_sql, __MODULE__.DynamicRepo, Application.compile_env(:ecto_sql, TestRepo))
|
||
|
|
||
|
defmodule DynamicRepo do
|
||
|
use Ecto.Repo, otp_app: :ecto_sql, adapter: TestRepo.__adapter__()
|
||
|
end
|
||
|
|
||
|
describe "errors" do
|
||
|
test "raises if repo doesn't exist" do
|
||
|
assert_raise UndefinedFunctionError, ~r"function UnknownRepo.get_dynamic_repo/0 is undefined", fn ->
|
||
|
Sandbox.mode(UnknownRepo, :manual)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "raises if repo is not started" do
|
||
|
assert_raise RuntimeError, ~r"could not lookup Ecto repo #{inspect DynamicRepo} because it was not started", fn ->
|
||
|
Sandbox.mode(DynamicRepo, :manual)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "raises if repo is not using sandbox" do
|
||
|
assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn ->
|
||
|
Sandbox.mode(PoolRepo, :manual)
|
||
|
end
|
||
|
|
||
|
assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn ->
|
||
|
Sandbox.checkout(PoolRepo)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "includes link to SQL sandbox on ownership errors" do
|
||
|
assert_raise DBConnection.OwnershipError,
|
||
|
~r"See Ecto.Adapters.SQL.Sandbox docs for more information.", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe "mode" do
|
||
|
test "uses the repository when checked out" do
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
assert TestRepo.all(Post) == []
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "uses the repository when allowed from another process" do
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
parent = self()
|
||
|
|
||
|
Task.start_link fn ->
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
Sandbox.allow(TestRepo, self(), parent)
|
||
|
send(parent, :allowed)
|
||
|
Process.sleep(:infinity)
|
||
|
end
|
||
|
|
||
|
assert_receive :allowed
|
||
|
assert TestRepo.all(Post) == []
|
||
|
end
|
||
|
|
||
|
test "uses the repository when allowed from another process by registered name" do
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
parent = self()
|
||
|
Process.register(parent, __MODULE__)
|
||
|
|
||
|
Task.start_link fn ->
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
Sandbox.allow(TestRepo, self(), __MODULE__)
|
||
|
send(parent, :allowed)
|
||
|
Process.sleep(:infinity)
|
||
|
end
|
||
|
|
||
|
assert_receive :allowed
|
||
|
assert TestRepo.all(Post) == []
|
||
|
|
||
|
Process.unregister(__MODULE__)
|
||
|
end
|
||
|
|
||
|
test "uses the repository when shared from another process" do
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
parent = self()
|
||
|
|
||
|
Task.start_link(fn ->
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
Sandbox.mode(TestRepo, {:shared, self()})
|
||
|
send(parent, :shared)
|
||
|
Process.sleep(:infinity)
|
||
|
end)
|
||
|
|
||
|
assert_receive :shared
|
||
|
assert Task.async(fn -> TestRepo.all(Post) end) |> Task.await == []
|
||
|
after
|
||
|
Sandbox.mode(TestRepo, :manual)
|
||
|
end
|
||
|
|
||
|
test "works with a dynamic repo" do
|
||
|
repo_pid = start_supervised!({DynamicRepo, name: nil})
|
||
|
DynamicRepo.put_dynamic_repo(repo_pid)
|
||
|
|
||
|
assert Sandbox.mode(DynamicRepo, :manual) == :ok
|
||
|
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
DynamicRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
Sandbox.checkout(DynamicRepo)
|
||
|
assert DynamicRepo.all(Post) == []
|
||
|
end
|
||
|
|
||
|
test "works with a repo pid" do
|
||
|
repo_pid = start_supervised!({DynamicRepo, name: nil})
|
||
|
DynamicRepo.put_dynamic_repo(repo_pid)
|
||
|
|
||
|
assert Sandbox.mode(repo_pid, :manual) == :ok
|
||
|
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
DynamicRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
Sandbox.checkout(repo_pid)
|
||
|
assert DynamicRepo.all(Post) == []
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe "savepoints" do
|
||
|
test "runs inside a sandbox that is rolled back on checkin" do
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
assert TestRepo.insert(%Post{})
|
||
|
assert TestRepo.all(Post) != []
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
assert TestRepo.all(Post) == []
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
end
|
||
|
|
||
|
test "runs inside a sandbox that may be disabled" do
|
||
|
Sandbox.checkout(TestRepo, sandbox: false)
|
||
|
assert TestRepo.insert(%Post{})
|
||
|
assert TestRepo.all(Post) != []
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
assert {1, _} = TestRepo.delete_all(Post)
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
|
||
|
Sandbox.checkout(TestRepo, sandbox: false)
|
||
|
assert {1, _} = TestRepo.delete_all(Post)
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
end
|
||
|
|
||
|
test "runs inside a sandbox with caller data when preloading associations" do
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
assert TestRepo.insert(%Post{})
|
||
|
parent = self()
|
||
|
|
||
|
Task.start_link fn ->
|
||
|
Sandbox.allow(TestRepo, parent, self())
|
||
|
assert [_] = TestRepo.all(Post) |> TestRepo.preload([:author, :comments])
|
||
|
send parent, :success
|
||
|
end
|
||
|
|
||
|
assert_receive :success
|
||
|
end
|
||
|
|
||
|
test "runs inside a sidebox with custom ownership timeout" do
|
||
|
:ok = Sandbox.checkout(TestRepo, ownership_timeout: 200)
|
||
|
parent = self()
|
||
|
|
||
|
assert capture_log(fn ->
|
||
|
{:ok, pid} =
|
||
|
Task.start(fn ->
|
||
|
Sandbox.allow(TestRepo, parent, self())
|
||
|
TestRepo.transaction(fn -> Process.sleep(500) end)
|
||
|
end)
|
||
|
|
||
|
ref = Process.monitor(pid)
|
||
|
assert_receive {:DOWN, ^ref, _, ^pid, _}, 1000
|
||
|
end) =~ "it owned the connection for longer than 200ms"
|
||
|
end
|
||
|
|
||
|
test "does not taint the sandbox on query errors" do
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
|
||
|
{:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true)
|
||
|
{:error, _} = TestRepo.query("INVALID")
|
||
|
{:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true)
|
||
|
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe "transactions" do
|
||
|
@tag :transaction_isolation
|
||
|
test "with custom isolation level" do
|
||
|
Sandbox.checkout(TestRepo, isolation: "READ UNCOMMITTED")
|
||
|
|
||
|
# Setting it to the same level later on works
|
||
|
TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
|
||
|
|
||
|
# Even inside a transaction
|
||
|
TestRepo.transaction fn ->
|
||
|
TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "disconnects on transaction timeouts" do
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
|
||
|
assert capture_log(fn ->
|
||
|
{:error, :rollback} =
|
||
|
TestRepo.transaction(fn -> Process.sleep(1000) end, timeout: 100)
|
||
|
end) =~ "timed out"
|
||
|
|
||
|
Sandbox.checkin(TestRepo)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe "checkouts" do
|
||
|
test "with transaction inside checkout" do
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
refute TestRepo.checked_out?()
|
||
|
refute TestRepo.in_transaction?()
|
||
|
|
||
|
TestRepo.checkout(fn ->
|
||
|
assert TestRepo.checked_out?()
|
||
|
refute TestRepo.in_transaction?()
|
||
|
TestRepo.transaction(fn ->
|
||
|
assert TestRepo.checked_out?()
|
||
|
assert TestRepo.in_transaction?()
|
||
|
end)
|
||
|
assert TestRepo.checked_out?()
|
||
|
refute TestRepo.in_transaction?()
|
||
|
end)
|
||
|
|
||
|
refute TestRepo.checked_out?()
|
||
|
refute TestRepo.in_transaction?()
|
||
|
end
|
||
|
|
||
|
test "with checkout inside transaction" do
|
||
|
Sandbox.checkout(TestRepo)
|
||
|
refute TestRepo.checked_out?()
|
||
|
refute TestRepo.in_transaction?()
|
||
|
|
||
|
TestRepo.transaction(fn ->
|
||
|
assert TestRepo.checked_out?()
|
||
|
assert TestRepo.in_transaction?()
|
||
|
TestRepo.checkout(fn ->
|
||
|
assert TestRepo.checked_out?()
|
||
|
assert TestRepo.in_transaction?()
|
||
|
end)
|
||
|
assert TestRepo.checked_out?()
|
||
|
assert TestRepo.in_transaction?()
|
||
|
end)
|
||
|
|
||
|
refute TestRepo.checked_out?()
|
||
|
refute TestRepo.in_transaction?()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe "start_owner!/2" do
|
||
|
test "checks out the connection" do
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
owner = Sandbox.start_owner!(TestRepo)
|
||
|
assert TestRepo.all(Post) == []
|
||
|
|
||
|
:ok = Sandbox.stop_owner(owner)
|
||
|
refute Process.alive?(owner)
|
||
|
end
|
||
|
|
||
|
test "can set shared mode" do
|
||
|
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
|
||
|
TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
parent = self()
|
||
|
|
||
|
Task.start_link(fn ->
|
||
|
owner = Sandbox.start_owner!(TestRepo, shared: true)
|
||
|
send(parent, {:owner, owner})
|
||
|
Process.sleep(:infinity)
|
||
|
end)
|
||
|
|
||
|
assert_receive {:owner, owner}
|
||
|
assert TestRepo.all(Post) == []
|
||
|
:ok = Sandbox.stop_owner(owner)
|
||
|
after
|
||
|
Sandbox.mode(TestRepo, :manual)
|
||
|
end
|
||
|
end
|
||
|
end
|