sandbox.exs (9290B)
1 defmodule Ecto.Integration.SandboxTest do 2 use ExUnit.Case 3 4 alias Ecto.Adapters.SQL.Sandbox 5 alias Ecto.Integration.{PoolRepo, TestRepo} 6 alias Ecto.Integration.Post 7 8 import ExUnit.CaptureLog 9 10 Application.put_env(:ecto_sql, __MODULE__.DynamicRepo, Application.compile_env(:ecto_sql, TestRepo)) 11 12 defmodule DynamicRepo do 13 use Ecto.Repo, otp_app: :ecto_sql, adapter: TestRepo.__adapter__() 14 end 15 16 describe "errors" do 17 test "raises if repo doesn't exist" do 18 assert_raise UndefinedFunctionError, ~r"function UnknownRepo.get_dynamic_repo/0 is undefined", fn -> 19 Sandbox.mode(UnknownRepo, :manual) 20 end 21 end 22 23 test "raises if repo is not started" do 24 assert_raise RuntimeError, ~r"could not lookup Ecto repo #{inspect DynamicRepo} because it was not started", fn -> 25 Sandbox.mode(DynamicRepo, :manual) 26 end 27 end 28 29 test "raises if repo is not using sandbox" do 30 assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn -> 31 Sandbox.mode(PoolRepo, :manual) 32 end 33 34 assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn -> 35 Sandbox.checkout(PoolRepo) 36 end 37 end 38 39 test "includes link to SQL sandbox on ownership errors" do 40 assert_raise DBConnection.OwnershipError, 41 ~r"See Ecto.Adapters.SQL.Sandbox docs for more information.", fn -> 42 TestRepo.all(Post) 43 end 44 end 45 end 46 47 describe "mode" do 48 test "uses the repository when checked out" do 49 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 50 TestRepo.all(Post) 51 end 52 Sandbox.checkout(TestRepo) 53 assert TestRepo.all(Post) == [] 54 Sandbox.checkin(TestRepo) 55 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 56 TestRepo.all(Post) 57 end 58 end 59 60 test "uses the repository when allowed from another process" do 61 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 62 TestRepo.all(Post) 63 end 64 65 parent = self() 66 67 Task.start_link fn -> 68 Sandbox.checkout(TestRepo) 69 Sandbox.allow(TestRepo, self(), parent) 70 send(parent, :allowed) 71 Process.sleep(:infinity) 72 end 73 74 assert_receive :allowed 75 assert TestRepo.all(Post) == [] 76 end 77 78 test "uses the repository when allowed from another process by registered name" do 79 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 80 TestRepo.all(Post) 81 end 82 83 parent = self() 84 Process.register(parent, __MODULE__) 85 86 Task.start_link fn -> 87 Sandbox.checkout(TestRepo) 88 Sandbox.allow(TestRepo, self(), __MODULE__) 89 send(parent, :allowed) 90 Process.sleep(:infinity) 91 end 92 93 assert_receive :allowed 94 assert TestRepo.all(Post) == [] 95 96 Process.unregister(__MODULE__) 97 end 98 99 test "uses the repository when shared from another process" do 100 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 101 TestRepo.all(Post) 102 end 103 104 parent = self() 105 106 Task.start_link(fn -> 107 Sandbox.checkout(TestRepo) 108 Sandbox.mode(TestRepo, {:shared, self()}) 109 send(parent, :shared) 110 Process.sleep(:infinity) 111 end) 112 113 assert_receive :shared 114 assert Task.async(fn -> TestRepo.all(Post) end) |> Task.await == [] 115 after 116 Sandbox.mode(TestRepo, :manual) 117 end 118 119 test "works with a dynamic repo" do 120 repo_pid = start_supervised!({DynamicRepo, name: nil}) 121 DynamicRepo.put_dynamic_repo(repo_pid) 122 123 assert Sandbox.mode(DynamicRepo, :manual) == :ok 124 125 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 126 DynamicRepo.all(Post) 127 end 128 129 Sandbox.checkout(DynamicRepo) 130 assert DynamicRepo.all(Post) == [] 131 end 132 133 test "works with a repo pid" do 134 repo_pid = start_supervised!({DynamicRepo, name: nil}) 135 DynamicRepo.put_dynamic_repo(repo_pid) 136 137 assert Sandbox.mode(repo_pid, :manual) == :ok 138 139 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 140 DynamicRepo.all(Post) 141 end 142 143 Sandbox.checkout(repo_pid) 144 assert DynamicRepo.all(Post) == [] 145 end 146 end 147 148 describe "savepoints" do 149 test "runs inside a sandbox that is rolled back on checkin" do 150 Sandbox.checkout(TestRepo) 151 assert TestRepo.insert(%Post{}) 152 assert TestRepo.all(Post) != [] 153 Sandbox.checkin(TestRepo) 154 Sandbox.checkout(TestRepo) 155 assert TestRepo.all(Post) == [] 156 Sandbox.checkin(TestRepo) 157 end 158 159 test "runs inside a sandbox that may be disabled" do 160 Sandbox.checkout(TestRepo, sandbox: false) 161 assert TestRepo.insert(%Post{}) 162 assert TestRepo.all(Post) != [] 163 Sandbox.checkin(TestRepo) 164 165 Sandbox.checkout(TestRepo) 166 assert {1, _} = TestRepo.delete_all(Post) 167 Sandbox.checkin(TestRepo) 168 169 Sandbox.checkout(TestRepo, sandbox: false) 170 assert {1, _} = TestRepo.delete_all(Post) 171 Sandbox.checkin(TestRepo) 172 end 173 174 test "runs inside a sandbox with caller data when preloading associations" do 175 Sandbox.checkout(TestRepo) 176 assert TestRepo.insert(%Post{}) 177 parent = self() 178 179 Task.start_link fn -> 180 Sandbox.allow(TestRepo, parent, self()) 181 assert [_] = TestRepo.all(Post) |> TestRepo.preload([:author, :comments]) 182 send parent, :success 183 end 184 185 assert_receive :success 186 end 187 188 test "runs inside a sidebox with custom ownership timeout" do 189 :ok = Sandbox.checkout(TestRepo, ownership_timeout: 200) 190 parent = self() 191 192 assert capture_log(fn -> 193 {:ok, pid} = 194 Task.start(fn -> 195 Sandbox.allow(TestRepo, parent, self()) 196 TestRepo.transaction(fn -> Process.sleep(500) end) 197 end) 198 199 ref = Process.monitor(pid) 200 assert_receive {:DOWN, ^ref, _, ^pid, _}, 1000 201 end) =~ "it owned the connection for longer than 200ms" 202 end 203 204 test "does not taint the sandbox on query errors" do 205 Sandbox.checkout(TestRepo) 206 207 {:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true) 208 {:error, _} = TestRepo.query("INVALID") 209 {:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true) 210 211 Sandbox.checkin(TestRepo) 212 end 213 end 214 215 describe "transactions" do 216 @tag :transaction_isolation 217 test "with custom isolation level" do 218 Sandbox.checkout(TestRepo, isolation: "READ UNCOMMITTED") 219 220 # Setting it to the same level later on works 221 TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED") 222 223 # Even inside a transaction 224 TestRepo.transaction fn -> 225 TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED") 226 end 227 end 228 229 test "disconnects on transaction timeouts" do 230 Sandbox.checkout(TestRepo) 231 232 assert capture_log(fn -> 233 {:error, :rollback} = 234 TestRepo.transaction(fn -> Process.sleep(1000) end, timeout: 100) 235 end) =~ "timed out" 236 237 Sandbox.checkin(TestRepo) 238 end 239 end 240 241 describe "checkouts" do 242 test "with transaction inside checkout" do 243 Sandbox.checkout(TestRepo) 244 refute TestRepo.checked_out?() 245 refute TestRepo.in_transaction?() 246 247 TestRepo.checkout(fn -> 248 assert TestRepo.checked_out?() 249 refute TestRepo.in_transaction?() 250 TestRepo.transaction(fn -> 251 assert TestRepo.checked_out?() 252 assert TestRepo.in_transaction?() 253 end) 254 assert TestRepo.checked_out?() 255 refute TestRepo.in_transaction?() 256 end) 257 258 refute TestRepo.checked_out?() 259 refute TestRepo.in_transaction?() 260 end 261 262 test "with checkout inside transaction" do 263 Sandbox.checkout(TestRepo) 264 refute TestRepo.checked_out?() 265 refute TestRepo.in_transaction?() 266 267 TestRepo.transaction(fn -> 268 assert TestRepo.checked_out?() 269 assert TestRepo.in_transaction?() 270 TestRepo.checkout(fn -> 271 assert TestRepo.checked_out?() 272 assert TestRepo.in_transaction?() 273 end) 274 assert TestRepo.checked_out?() 275 assert TestRepo.in_transaction?() 276 end) 277 278 refute TestRepo.checked_out?() 279 refute TestRepo.in_transaction?() 280 end 281 end 282 283 describe "start_owner!/2" do 284 test "checks out the connection" do 285 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 286 TestRepo.all(Post) 287 end 288 289 owner = Sandbox.start_owner!(TestRepo) 290 assert TestRepo.all(Post) == [] 291 292 :ok = Sandbox.stop_owner(owner) 293 refute Process.alive?(owner) 294 end 295 296 test "can set shared mode" do 297 assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn -> 298 TestRepo.all(Post) 299 end 300 301 parent = self() 302 303 Task.start_link(fn -> 304 owner = Sandbox.start_owner!(TestRepo, shared: true) 305 send(parent, {:owner, owner}) 306 Process.sleep(:infinity) 307 end) 308 309 assert_receive {:owner, owner} 310 assert TestRepo.all(Post) == [] 311 :ok = Sandbox.stop_owner(owner) 312 after 313 Sandbox.mode(TestRepo, :manual) 314 end 315 end 316 end