diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f75a707eb..c8b6a4e87 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -32,7 +32,7 @@ jobs: - name: Check warnings run: mix compile --warnings-as-errors - name: Run tests - run: elixir --cookie "COOKIEFORTESTS" -S mix test + run: mix test - name: Install Node uses: actions/setup-node@v2 with: @@ -50,3 +50,41 @@ jobs: run: npm run format-check --prefix assets - name: Run assets tests run: npm test --prefix assets + windows: + runs-on: windows-latest + if: github.ref == 'refs/heads/main' + env: + MIX_ENV: test + steps: + - name: Configure Git + run: git config --global core.autocrlf input + - uses: actions/checkout@v2 + - name: Install Erlang & Elixir + uses: erlef/setup-beam@v1 + with: + otp-version: '24.0' + elixir-version: '1.13.0' + - name: Start epmd + run: cmd /c "START /b epmd" + working-directory: ${{ env.INSTALL_DIR_FOR_OTP }}/erts-12.0.4/bin + # Add tar that supports symlinks, see https://github.com/actions/virtual-environments/issues/4679 + - name: Add tar.exe + run: | + "C:\Program Files\Git\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 + - name: Cache Mix + uses: actions/cache@v2 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix- + - name: Install mix dependencies + run: mix deps.get + - name: Check formatting + run: mix format --check-formatted + - name: Check warnings + run: mix compile --warnings-as-errors + - name: Run tests + run: mix test diff --git a/lib/livebook/file_system/local.ex b/lib/livebook/file_system/local.ex index c455fd2a0..41f9be96b 100644 --- a/lib/livebook/file_system/local.ex +++ b/lib/livebook/file_system/local.ex @@ -168,7 +168,7 @@ defimpl Livebook.FileSystem, for: Livebook.FileSystem.Local do containing_dir = Path.dirname(destination_path) with :ok <- File.mkdir_p(containing_dir), - :ok <- File.rename(source_path, destination_path) do + :ok <- rename_or_move(source_path, destination_path) do :ok else {:error, error} -> @@ -178,6 +178,18 @@ defimpl Livebook.FileSystem, for: Livebook.FileSystem.Local do end end + defp rename_or_move(source_path, destination_path) do + with {:error, :exdev} <- File.rename(source_path, destination_path) do + # For files on different file systems, try to copy and remove instead + with {:ok, _paths} <- File.cp_r(source_path, destination_path), + {:ok, _paths} <- File.rm_rf(source_path) do + :ok + else + {:error, error, _paths} -> {:error, error} + end + end + end + def etag_for(file_system, path) do with :ok <- ensure_local(file_system) do case File.stat(path) do diff --git a/lib/livebook/file_system/utils.ex b/lib/livebook/file_system/utils.ex index 8bdfbfd6d..4568ed871 100644 --- a/lib/livebook/file_system/utils.ex +++ b/lib/livebook/file_system/utils.ex @@ -86,8 +86,8 @@ defmodule Livebook.FileSystem.Utils do and handles sequences such as "." and "..". """ @spec resolve_unix_like_path(FileSystem.path(), String.t()) :: FileSystem.t() - def resolve_unix_like_path(dir_path, subject) do - assert_dir_path!(dir_path) + def resolve_unix_like_path(relative_to, subject) do + dir_path = relative_to |> Path.dirname() |> ensure_dir_path() subject = if Path.basename(subject) in [".", ".."] do diff --git a/lib/livebook/utils.ex b/lib/livebook/utils.ex index 41dd7d215..e9dea591c 100644 --- a/lib/livebook/utils.ex +++ b/lib/livebook/utils.ex @@ -249,7 +249,7 @@ defmodule Livebook.Utils do url |> URI.parse() |> Map.update!(:path, fn path -> - path |> Path.dirname() |> Path.join(relative_path) |> Path.expand() + Livebook.FileSystem.Utils.resolve_unix_like_path(path, relative_path) end) |> URI.to_string() end @@ -444,20 +444,20 @@ defmodule Livebook.Utils do end @doc """ - Returns a URL (including localhost) to open the given `url` as a notebook + Returns a URL (including localhost) to open the given `path` as a notebook - iex> Livebook.Utils.notebook_open_url("https://example.com/foo.livemd") - "http://localhost:4002/open?path=https%3A%2F%2Fexample.com%2Ffoo.livemd" + iex> Livebook.Utils.notebook_open_url("/data/foo.livemd") + "http://localhost:4002/open?path=%2Fdata%2Ffoo.livemd" - iex> Livebook.Utils.notebook_open_url("https://my_host", "https://example.com/foo.livemd") - "https://my_host/open?path=https%3A%2F%2Fexample.com%2Ffoo.livemd" + iex> Livebook.Utils.notebook_open_url("https://my_host", "/data/foo.livemd") + "https://my_host/open?path=%2Fdata%2Ffoo.livemd" """ - def notebook_open_url(base_url \\ LivebookWeb.Endpoint.access_struct_url(), url) do + def notebook_open_url(base_url \\ LivebookWeb.Endpoint.access_struct_url(), path) do base_url |> URI.parse() |> Map.replace!(:path, "/open") - |> append_query("path=#{URI.encode_www_form(url)}") + |> append_query("path=#{URI.encode_www_form(path)}") |> URI.to_string() end diff --git a/test/livebook/file_system/file_test.exs b/test/livebook/file_system/file_test.exs index e08d01e29..d2e4699a5 100644 --- a/test/livebook/file_system/file_test.exs +++ b/test/livebook/file_system/file_test.exs @@ -18,60 +18,61 @@ defmodule Livebook.FileSystem.FileTest do file_system = FileSystem.Local.new() assert_raise ArgumentError, - ~s{expected an expanded absolute path, got: "/dir/nested/../file.txt"}, + ~s{expected an expanded absolute path, got: "#{p("/dir/nested/../file.txt")}"}, fn -> - FileSystem.File.new(file_system, "/dir/nested/../file.txt") + FileSystem.File.new(file_system, p("/dir/nested/../file.txt")) end end test "uses default file system path if non is given" do - file_system = FileSystem.Local.new(default_path: "/dir/") - assert %FileSystem.File{path: "/dir/"} = FileSystem.File.new(file_system) + default_path = p("/dir/") + file_system = FileSystem.Local.new(default_path: default_path) + assert %FileSystem.File{path: ^default_path} = FileSystem.File.new(file_system) end end describe "local/1" do test "uses the globally configured local file system instance" do - assert FileSystem.File.local("/path").file_system == Livebook.Config.local_filesystem() + assert FileSystem.File.local(p("/path")).file_system == Livebook.Config.local_filesystem() end end describe "relative/2" do test "ignores the file path if an absolute path is given" do file_system = FileSystem.Local.new() - file = FileSystem.File.new(file_system, "/dir/nested/file.txt") + file = FileSystem.File.new(file_system, p("/dir/nested/file.txt")) - assert %FileSystem.File{file_system: ^file_system, path: "/other/file.txt"} = - FileSystem.File.resolve(file, "/other/file.txt") + assert %FileSystem.File{file_system: ^file_system, path: p("/other/file.txt")} = + FileSystem.File.resolve(file, p("/other/file.txt")) end test "resolves a relative path against a regular file" do file_system = FileSystem.Local.new() - file = FileSystem.File.new(file_system, "/dir/nested/file.txt") + file = FileSystem.File.new(file_system, p("/dir/nested/file.txt")) - assert %FileSystem.File{file_system: ^file_system, path: "/dir/other/other_file.txt"} = + assert %FileSystem.File{file_system: ^file_system, path: p("/dir/other/other_file.txt")} = FileSystem.File.resolve(file, "../other/other_file.txt") end test "resolves a relative path against a directory file" do file_system = FileSystem.Local.new() - dir = FileSystem.File.new(file_system, "/dir/nested/") + dir = FileSystem.File.new(file_system, p("/dir/nested/")) - assert %FileSystem.File{file_system: ^file_system, path: "/dir/nested/file.txt"} = + assert %FileSystem.File{file_system: ^file_system, path: p("/dir/nested/file.txt")} = FileSystem.File.resolve(dir, "file.txt") end test "resolves a relative directory path" do file_system = FileSystem.Local.new() - file = FileSystem.File.new(file_system, "/dir/nested/file.txt") + file = FileSystem.File.new(file_system, p("/dir/nested/file.txt")) - assert %FileSystem.File{file_system: ^file_system, path: "/dir/other/"} = + assert %FileSystem.File{file_system: ^file_system, path: p("/dir/other/")} = FileSystem.File.resolve(file, "../other/") - assert %FileSystem.File{file_system: ^file_system, path: "/dir/nested/"} = + assert %FileSystem.File{file_system: ^file_system, path: p("/dir/nested/")} = FileSystem.File.resolve(file, ".") - assert %FileSystem.File{file_system: ^file_system, path: "/dir/"} = + assert %FileSystem.File{file_system: ^file_system, path: p("/dir/")} = FileSystem.File.resolve(file, "..") end end @@ -80,10 +81,10 @@ defmodule Livebook.FileSystem.FileTest do test "returns true if file path has a trailing slash" do file_system = FileSystem.Local.new() - dir = FileSystem.File.new(file_system, "/dir/") + dir = FileSystem.File.new(file_system, p("/dir/")) assert FileSystem.File.dir?(dir) - file = FileSystem.File.new(file_system, "/dir/file.txt") + file = FileSystem.File.new(file_system, p("/dir/file.txt")) refute FileSystem.File.dir?(file) end end @@ -92,10 +93,10 @@ defmodule Livebook.FileSystem.FileTest do test "returns true if file path has no trailing slash" do file_system = FileSystem.Local.new() - dir = FileSystem.File.new(file_system, "/dir/") + dir = FileSystem.File.new(file_system, p("/dir/")) refute FileSystem.File.regular?(dir) - file = FileSystem.File.new(file_system, "/dir/file.txt") + file = FileSystem.File.new(file_system, p("/dir/file.txt")) assert FileSystem.File.regular?(file) end end @@ -104,10 +105,10 @@ defmodule Livebook.FileSystem.FileTest do test "returns path basename" do file_system = FileSystem.Local.new() - dir = FileSystem.File.new(file_system, "/dir/") + dir = FileSystem.File.new(file_system, p("/dir/")) assert FileSystem.File.name(dir) == "dir" - file = FileSystem.File.new(file_system, "/dir/file.txt") + file = FileSystem.File.new(file_system, p("/dir/file.txt")) assert FileSystem.File.name(file) == "file.txt" end end @@ -116,21 +117,23 @@ defmodule Livebook.FileSystem.FileTest do test "given a directory, returns the parent directory" do file_system = FileSystem.Local.new() - dir = FileSystem.File.new(file_system, "/parent/dir/") - assert FileSystem.File.new(file_system, "/parent/") == FileSystem.File.containing_dir(dir) + dir = FileSystem.File.new(file_system, p("/parent/dir/")) + + assert FileSystem.File.new(file_system, p("/parent/")) == + FileSystem.File.containing_dir(dir) end test "given a file, returns the containing directory" do file_system = FileSystem.Local.new() - file = FileSystem.File.new(file_system, "/dir/file.txt") - assert FileSystem.File.new(file_system, "/dir/") == FileSystem.File.containing_dir(file) + file = FileSystem.File.new(file_system, p("/dir/file.txt")) + assert FileSystem.File.new(file_system, p("/dir/")) == FileSystem.File.containing_dir(file) end test "given the root directory, returns itself" do file_system = FileSystem.Local.new() - file = FileSystem.File.new(file_system, "/") + file = FileSystem.File.new(file_system, p("/")) assert file == FileSystem.File.containing_dir(file) end end diff --git a/test/livebook/file_system/local_test.exs b/test/livebook/file_system/local_test.exs index 3403cd2c5..197dd1a02 100644 --- a/test/livebook/file_system/local_test.exs +++ b/test/livebook/file_system/local_test.exs @@ -8,9 +8,11 @@ defmodule Livebook.FileSystem.LocalTest do describe "new/1" do test "raises when :default_path is not a directory" do - assert_raise ArgumentError, ~s{expected a directory path, got: "/notebook.livemd"}, fn -> - Local.new(default_path: "/notebook.livemd") - end + assert_raise ArgumentError, + ~s{expected a directory path, got: "#{p("/notebook.livemd")}"}, + fn -> + Local.new(default_path: p("/notebook.livemd")) + end end end @@ -21,8 +23,8 @@ defmodule Livebook.FileSystem.LocalTest do end test "returns custom directory path if configured" do - file_system = Local.new(default_path: "/dir/") - assert FileSystem.default_path(file_system) == "/dir/" + file_system = Local.new(default_path: p("/dir/")) + assert FileSystem.default_path(file_system) == p("/dir/") end end @@ -489,25 +491,29 @@ defmodule Livebook.FileSystem.LocalTest do test "resolves relative paths" do file_system = Local.new() - assert "/dir/" = FileSystem.resolve_path(file_system, "/dir/", "") - assert "/dir/file.txt" = FileSystem.resolve_path(file_system, "/dir/", "file.txt") - assert "/dir/nested/" = FileSystem.resolve_path(file_system, "/dir/", "nested/") - assert "/dir/" = FileSystem.resolve_path(file_system, "/dir/", ".") - assert "/" = FileSystem.resolve_path(file_system, "/dir/", "..") + assert p("/dir/") = FileSystem.resolve_path(file_system, p("/dir/"), "") + assert p("/dir/file.txt") = FileSystem.resolve_path(file_system, p("/dir/"), "file.txt") + assert p("/dir/nested/") = FileSystem.resolve_path(file_system, p("/dir/"), "nested/") + assert p("/dir/") = FileSystem.resolve_path(file_system, p("/dir/"), ".") + assert p("/") = FileSystem.resolve_path(file_system, p("/dir/"), "..") - assert "/file.txt" = - FileSystem.resolve_path(file_system, "/dir/", "nested/../.././file.txt") + assert p("/file.txt") = + FileSystem.resolve_path(file_system, p("/dir/"), "nested/../.././file.txt") end test "resolves absolute paths" do file_system = Local.new() - assert "/" = FileSystem.resolve_path(file_system, "/dir/", "/") - assert "/file.txt" = FileSystem.resolve_path(file_system, "/dir/", "/file.txt") - assert "/nested/" = FileSystem.resolve_path(file_system, "/dir/", "/nested/") + assert p("/") = FileSystem.resolve_path(file_system, p("/dir/"), p("/")) + assert p("/file.txt") = FileSystem.resolve_path(file_system, p("/dir/"), p("/file.txt")) + assert p("/nested/") = FileSystem.resolve_path(file_system, p("/dir/"), p("/nested/")) - assert "/nested/file.txt" = - FileSystem.resolve_path(file_system, "/dir/", "///nested///other/..///file.txt") + assert p("/nested/file.txt") = + FileSystem.resolve_path( + file_system, + p("/dir/"), + p("///nested///other/..///file.txt") + ) end end end diff --git a/test/livebook/session/data_test.exs b/test/livebook/session/data_test.exs index 108ac75b1..3f20af258 100644 --- a/test/livebook/session/data_test.exs +++ b/test/livebook/session/data_test.exs @@ -3451,7 +3451,7 @@ defmodule Livebook.Session.DataTest do test "updates data with the given path" do data = Data.new() - file = Livebook.FileSystem.File.local("/path/to/file.livemd") + file = Livebook.FileSystem.File.local(p("/path/to/file.livemd")) operation = {:set_file, self(), file} assert {:ok, %{file: ^file}, []} = Data.apply_operation(data, operation) diff --git a/test/livebook/session/file_guard_test.exs b/test/livebook/session/file_guard_test.exs index 0d2bf2f13..cdfd7700a 100644 --- a/test/livebook/session/file_guard_test.exs +++ b/test/livebook/session/file_guard_test.exs @@ -1,30 +1,32 @@ defmodule Livebook.Session.FileGuardTest do use ExUnit.Case, async: false + import Livebook.TestHelpers + alias Livebook.Session.FileGuard alias Livebook.FileSystem test "lock/2 returns an error if the given file is already locked" do - file = FileSystem.File.local("/some/path") + file = FileSystem.File.local(p("/some/path")) assert :ok = FileGuard.lock(file, self()) assert {:error, :already_in_use} = FileGuard.lock(file, self()) end test "lock/2 is agnostic to irrelevant file system configuration" do - fs1 = FileSystem.Local.new(default_path: "/path/1/") - fs2 = FileSystem.Local.new(default_path: "/path/2/") + fs1 = FileSystem.Local.new(default_path: p("/path/1/")) + fs2 = FileSystem.Local.new(default_path: p("/path/2/")) # The file system has different configuration, but it's the same resource - file1 = FileSystem.File.new(fs1, "/some/path") - file2 = FileSystem.File.new(fs2, "/some/path") + file1 = FileSystem.File.new(fs1, p("/some/path")) + file2 = FileSystem.File.new(fs2, p("/some/path")) assert :ok = FileGuard.lock(file1, self()) assert {:error, :already_in_use} = FileGuard.lock(file2, self()) end test "unlock/1 unlocks the given file" do - file = FileSystem.File.local("/some/path") + file = FileSystem.File.local(p("/some/path")) assert :ok = FileGuard.lock(file, self()) :ok = FileGuard.unlock(file) @@ -32,7 +34,7 @@ defmodule Livebook.Session.FileGuardTest do end test "file is automatically unloacked when the owner process termiantes" do - file = FileSystem.File.local("/some/path") + file = FileSystem.File.local(p("/some/path")) owner = spawn(fn -> :ok end) :ok = FileGuard.lock(file, owner) diff --git a/test/livebook/session_test.exs b/test/livebook/session_test.exs index 027d95b98..036c67f7a 100644 --- a/test/livebook/session_test.exs +++ b/test/livebook/session_test.exs @@ -346,7 +346,7 @@ defmodule Livebook.SessionTest do Session.set_file(session.pid, file) # Wait for the session to deal with the files - Process.sleep(100) + Process.sleep(500) assert {:ok, true} = FileSystem.File.exists?(FileSystem.File.resolve(tmp_dir, "images/test.jpg")) @@ -368,7 +368,7 @@ defmodule Livebook.SessionTest do Session.set_file(session.pid, nil) # Wait for the session to deal with the files - Process.sleep(200) + Process.sleep(500) assert {:ok, true} = FileSystem.File.exists?(image_file) diff --git a/test/livebook_web/live/file_select_component_test.exs b/test/livebook_web/live/file_select_component_test.exs index 3597b59ba..dca197fc5 100644 --- a/test/livebook_web/live/file_select_component_test.exs +++ b/test/livebook_web/live/file_select_component_test.exs @@ -2,6 +2,7 @@ defmodule LivebookWeb.FileSelectComponentTest do use LivebookWeb.ConnCase, async: true import Phoenix.LiveViewTest + import Livebook.TestHelpers alias Livebook.FileSystem alias LivebookWeb.FileSelectComponent @@ -18,7 +19,7 @@ defmodule LivebookWeb.FileSelectComponentTest do end test "does not show parent directory when in root" do - file = FileSystem.File.local("/") + file = FileSystem.File.local(p("/")) refute render_component(FileSelectComponent, attrs(file: file)) =~ ".." end @@ -26,7 +27,7 @@ defmodule LivebookWeb.FileSelectComponentTest do Keyword.merge( [ id: 1, - file: FileSystem.File.local("/"), + file: FileSystem.File.local(p("/")), extnames: [".livemd"], running_files: [] ], diff --git a/test/livebook_web/live/home_live_test.exs b/test/livebook_web/live/home_live_test.exs index 323785a95..666c00c85 100644 --- a/test/livebook_web/live/home_live_test.exs +++ b/test/livebook_web/live/home_live_test.exs @@ -16,10 +16,12 @@ defmodule LivebookWeb.HomeLiveTest do assert {:error, {:live_redirect, %{to: to}}} = view - |> element("button", "New notebook") + |> element(~s/[role="navigation"] button/, "New notebook") |> render_click() assert to =~ "/sessions/" + + close_session_by_path(to) end describe "file selection" do @@ -55,6 +57,8 @@ defmodule LivebookWeb.HomeLiveTest do |> render_click() assert to =~ "/sessions/" + + close_session_by_path(to) end @tag :tmp_dir @@ -173,6 +177,7 @@ defmodule LivebookWeb.HomeLiveTest do {:ok, view, _} = live(conn, to) assert render(view) =~ "My notebook - fork" + close_session_by_path(to) Session.close(session.pid) end @@ -192,8 +197,6 @@ defmodule LivebookWeb.HomeLiveTest do |> render_click() refute render(view) =~ session.id - - Session.close(session.pid) end test "close all selected sessions using bulk action", %{conn: conn} do @@ -237,10 +240,10 @@ defmodule LivebookWeb.HomeLiveTest do |> render_click() |> follow_redirect(conn) - assert to =~ "/sessions/" - {:ok, view, _} = live(conn, to) assert render(view) =~ "Welcome to Livebook" + + close_session_by_path(to) end describe "notebook import" do @@ -255,10 +258,12 @@ defmodule LivebookWeb.HomeLiveTest do |> element("form", "Import") |> render_submit(%{data: %{content: notebook_content}}) - {path, _flash} = assert_redirect(view, 1000) + {path, _flash} = assert_redirect(view, 5000) {:ok, view, _} = live(conn, path) assert render(view) =~ "My notebook" + + close_session_by_path(path) end test "should show info flash with information about the imported notebook", %{conn: conn} do @@ -272,10 +277,12 @@ defmodule LivebookWeb.HomeLiveTest do |> element("form", "Import") |> render_submit(%{data: %{content: notebook_content}}) - {_path, flash} = assert_redirect(view, 1000) + {path, flash} = assert_redirect(view, 5000) assert flash["info"] =~ "You have imported a notebook, no code has been executed so far. You should read and evaluate code as needed." + + close_session_by_path(path) end test "should show warning flash when the imported notebook have errors", %{conn: conn} do @@ -292,10 +299,12 @@ defmodule LivebookWeb.HomeLiveTest do |> element("form", "Import") |> render_submit(%{data: %{content: notebook_content}}) - {_path, flash} = assert_redirect(view, 1000) + {path, flash} = assert_redirect(view, 5000) assert flash["warning"] =~ "We found problems while importing the file and tried to autofix them:\n- Downgrading all headings, because 3 instances of heading 1 were found" + + close_session_by_path(path) end end @@ -316,6 +325,8 @@ defmodule LivebookWeb.HomeLiveTest do {:ok, view, _} = live(conn, to) assert render(view) =~ "My notebook" + + close_session_by_path(to) end @tag :tmp_dir @@ -329,6 +340,8 @@ defmodule LivebookWeb.HomeLiveTest do {:ok, view, _} = live(conn, to) assert render(view) =~ "My notebook" + + close_session_by_path(to) end test "redirects to the import form on error", %{conn: conn} do @@ -363,13 +376,15 @@ defmodule LivebookWeb.HomeLiveTest do test "opens a file when livebook file is passed", %{conn: conn, tmp_dir: tmp_dir} do notebook_path = Path.join(tmp_dir, "notebook.livemd") - :ok = File.write(notebook_path, "# Notebook OPEN section") + :ok = File.write(notebook_path, "# My notebook") assert {:error, {:live_redirect, %{flash: %{}, to: to}}} = live(conn, "/open?path=#{notebook_path}") {:ok, view, _} = live(conn, to) - assert render(view) =~ "Notebook OPEN section" + assert render(view) =~ "My notebook" + + close_session_by_path(to) end end @@ -387,4 +402,9 @@ defmodule LivebookWeb.HomeLiveTest do path end + + defp close_session_by_path("/sessions/" <> session_id) do + {:ok, session} = Sessions.fetch_session(session_id) + Session.close(session.pid) + end end diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index 6ee990bc6..b70b0a93b 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -39,4 +39,9 @@ defmodule Livebook.TestHelpers do end end) end + + @doc """ + Converts a Unix-like absolute path into OS-compatible absolute path. + """ + defmacro p("/" <> path), do: Path.expand("/") <> path end diff --git a/test/test_helper.exs b/test/test_helper.exs index ca5f7050c..477bf8afa 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -45,4 +45,4 @@ erl_docs_available? = Code.fetch_docs(:gen_server) != {:error, :chunk_not_found} exclude = [] exclude = if erl_docs_available?, do: exclude, else: Keyword.put(exclude, :erl_docs, true) -ExUnit.start(assert_receive_timeout: 1_000, exclude: exclude) +ExUnit.start(assert_receive_timeout: 1_500, exclude: exclude)