livebook/test/live_book/session_test.exs

281 lines
9.7 KiB
Elixir

defmodule LiveBook.SessionTest do
use ExUnit.Case, async: true
alias LiveBook.{Session, Delta, Runtime, Utils}
setup do
session_id = start_session()
%{session_id: session_id}
end
describe "insert_section/2" do
test "sends an insert opreation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
Session.insert_section(session_id, 0)
assert_receive {:operation, {:insert_section, 0, _id}}
end
end
describe "insert_cell/4" do
test "sends an insert opreation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
Session.insert_section(session_id, 0)
assert_receive {:operation, {:insert_section, 0, section_id}}
Session.insert_cell(session_id, section_id, 0, :elixir)
assert_receive {:operation, {:insert_cell, ^section_id, 0, :elixir, _id}}
end
end
describe "delete_section/2" do
test "sends a delete opreation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{section_id, _cell_id} = insert_section_and_cell(session_id)
Session.delete_section(session_id, section_id)
assert_receive {:operation, {:delete_section, ^section_id}}
end
end
describe "delete_cell/2" do
test "sends a delete opreation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{_section_id, cell_id} = insert_section_and_cell(session_id)
Session.delete_cell(session_id, cell_id)
assert_receive {:operation, {:delete_cell, ^cell_id}}
end
end
describe "queue_cell_evaluation/2" do
test "sends a queue evaluation operation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{_section_id, cell_id} = insert_section_and_cell(session_id)
Session.queue_cell_evaluation(session_id, cell_id)
assert_receive {:operation, {:queue_cell_evaluation, ^cell_id}}
end
test "triggers evaluation and sends update operation once it finishes",
%{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{_section_id, cell_id} = insert_section_and_cell(session_id)
Session.queue_cell_evaluation(session_id, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, ^cell_id, _}}
end
end
describe "cancel_cell_evaluation/2" do
test "sends a cancel evaluation operation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{_section_id, cell_id} = insert_section_and_cell(session_id)
queue_evaluation(session_id, cell_id)
Session.cancel_cell_evaluation(session_id, cell_id)
assert_receive {:operation, {:cancel_cell_evaluation, ^cell_id}}
end
end
describe "set_notebook_name/2" do
test "sends a notebook name update operation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
Session.set_notebook_name(session_id, "Cat's guide to life")
assert_receive {:operation, {:set_notebook_name, "Cat's guide to life"}}
end
end
describe "set_section_name/3" do
test "sends a section name update operation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{section_id, _cell_id} = insert_section_and_cell(session_id)
Session.set_section_name(session_id, section_id, "Chapter 1")
assert_receive {:operation, {:set_section_name, ^section_id, "Chapter 1"}}
end
end
describe "apply_cell_delta/5" do
test "sends a cell delta operation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{_section_id, cell_id} = insert_section_and_cell(session_id)
from = self()
delta = Delta.new() |> Delta.insert("cats")
revision = 1
Session.apply_cell_delta(session_id, from, cell_id, delta, revision)
assert_receive {:operation, {:apply_cell_delta, ^from, ^cell_id, ^delta, ^revision}}
end
end
describe "connect_runtime/2" do
test "sends a runtime update operation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{:ok, runtime} = LiveBookTest.Runtime.SingleEvaluator.init()
Session.connect_runtime(session_id, runtime)
assert_receive {:operation, {:set_runtime, ^runtime}}
end
end
describe "disconnect_runtime/1" do
test "sends a runtime update operation to subscribers", %{session_id: session_id} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
Session.disconnect_runtime(session_id)
assert_receive {:operation, {:set_runtime, nil}}
end
end
describe "set_path/1" do
@tag :tmp_dir
test "sends a path update operation to subscribers",
%{session_id: session_id, tmp_dir: tmp_dir} do
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
path = Path.join(tmp_dir, "notebook.livemd")
Session.set_path(session_id, path)
assert_receive {:operation, {:set_path, ^path}}
end
@tag :tmp_dir
test "broadcasts an error if the path is already in use",
%{session_id: session_id, tmp_dir: tmp_dir} do
path = Path.join(tmp_dir, "notebook.livemd")
start_session(path: path)
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
Session.set_path(session_id, path)
assert_receive {:error, "failed to set new path because it is already in use"}
end
end
describe "save/1" do
@tag :tmp_dir
test "persists the notebook to the associated file and notifies subscribers",
%{session_id: session_id, tmp_dir: tmp_dir} do
path = Path.join(tmp_dir, "notebook.livemd")
Session.set_path(session_id, path)
# Perform a change, so the notebook is dirty
Session.set_notebook_name(session_id, "My notebook")
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
refute File.exists?(path)
Session.save(session_id)
assert_receive {:operation, :mark_as_not_dirty}
assert File.exists?(path)
assert File.read!(path) =~ "My notebook"
end
end
describe "close/1" do
@tag :tmp_dir
test "saves the notebook and notifies subscribers once the session is closed",
%{session_id: session_id, tmp_dir: tmp_dir} do
path = Path.join(tmp_dir, "notebook.livemd")
Session.set_path(session_id, path)
# Perform a change, so the notebook is dirty
Session.set_notebook_name(session_id, "My notebook")
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
refute File.exists?(path)
Process.flag(:trap_exit, true)
Session.close(session_id)
assert_receive :session_closed
assert File.exists?(path)
assert File.read!(path) =~ "My notebook"
end
end
describe "start_link/1" do
@tag :tmp_dir
test "fails if the given path is already in use", %{tmp_dir: tmp_dir} do
path = Path.join(tmp_dir, "notebook.livemd")
start_session(path: path)
assert {:error, "the given path is already in use"} ==
Session.start_link(id: Utils.random_id(), path: path)
end
end
# For most tests we use the lightweight runtime, so that they are cheap to run.
# Here go several integration tests that actually start a separate runtime
# to verify session integrates well with it.
test "starts a standalone runtime upon first evaluation if there was none set explicitly" do
session_id = Utils.random_id()
{:ok, _} = Session.start_link(id: session_id)
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
{_section_id, cell_id} = insert_section_and_cell(session_id)
Session.queue_cell_evaluation(session_id, cell_id)
# Give it a bit more time as this involves starting a system process.
assert_receive {:operation, {:add_cell_evaluation_response, ^cell_id, _}}, 1000
end
test "if the runtime node goes down, notifies the subscribers" do
session_id = Utils.random_id()
{:ok, _} = Session.start_link(id: session_id)
{:ok, runtime} = Runtime.Standalone.init(self())
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
# Wait for the runtime to best set
Session.connect_runtime(session_id, runtime)
assert_receive {:operation, {:set_runtime, ^runtime}}
# Terminate the other node, the session should detect that.
Node.spawn(runtime.node, System, :halt, [])
assert_receive {:operation, {:set_runtime, nil}}
assert_receive {:info, "runtime node terminated unexpectedly"}
end
defp start_session(opts \\ []) do
session_id = Utils.random_id()
{:ok, _} = Session.start_link(Keyword.merge(opts, id: session_id))
# By default, use the current node for evaluation,
# rather than starting a standalone one.
{:ok, runtime} = LiveBookTest.Runtime.SingleEvaluator.init()
Session.connect_runtime(session_id, runtime)
session_id
end
defp insert_section_and_cell(session_id) do
Session.insert_section(session_id, 0)
assert_receive {:operation, {:insert_section, 0, section_id}}
Session.insert_cell(session_id, section_id, 0, :elixir)
assert_receive {:operation, {:insert_cell, ^section_id, 0, :elixir, cell_id}}
{section_id, cell_id}
end
defp queue_evaluation(session_id, cell_id) do
Session.queue_cell_evaluation(session_id, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, ^cell_id, _}}
end
end