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
 |