Add support for Mix runtime as the default one (#334)

* Add support for Mix runtime as a default

* Support default runtime options in the CLI

* Set cell status to queued while runtime is being started

* Clean up tests
This commit is contained in:
Jonatan Kłosko 2021-06-09 16:24:02 +02:00 committed by GitHub
parent f1c2db4118
commit c4a96bc99c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 289 additions and 86 deletions

View file

@ -98,8 +98,8 @@ The following environment variables configure Livebook:
* LIVEBOOK_DEFAULT_RUNTIME - sets the runtime type that is used
by default when none is started explicitly for the given notebook.
Must be either "standalone" (Elixir standalone) or "embedded" (Embedded).
Defaults to "standalone".
Must be either "standalone" (Elixir standalone), "mix[:path]" (Mix standalone)
or "embedded" (Embedded). Defaults to "standalone".
* LIVEBOOK_IP - sets the ip address to start the web application on. Must be a valid IPv4 or IPv6 address.

View file

@ -20,10 +20,11 @@ config :livebook, :authentication_mode, :token
# Sets the default runtime to ElixirStandalone.
# This is the desired default most of the time,
# but in some specific use cases you may want
# to configure that to the Embedded runtime instead.
# to configure that to the Embedded or Mix runtime instead.
# Also make sure the configured runtime has
# a synchronous `init/0` method.
config :livebook, :default_runtime, Livebook.Runtime.ElixirStandalone
# a synchronous `init` function that takes the
# configured arguments.
config :livebook, :default_runtime, {Livebook.Runtime.ElixirStandalone, []}
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.

View file

@ -29,4 +29,4 @@ config :livebook,
config :livebook,
:default_runtime,
Livebook.Config.default_runtime!("LIVEBOOK_DEFAULT_RUNTIME") ||
Livebook.Runtime.ElixirStandalone
{Livebook.Runtime.ElixirStandalone, []}

View file

@ -15,7 +15,7 @@ config :livebook, :authentication_mode, :disabled
# Use the embedded runtime in tests by default, so they
# are cheaper to run. Other runtimes can be tested by starting
# and setting them explicitly
config :livebook, :default_runtime, Livebook.Runtime.Embedded
config :livebook, :default_runtime, {Livebook.Runtime.Embedded, []}
# Use longnames when running tests in CI, so that no host resolution is required,
# see https://github.com/elixir-nx/livebook/pull/173#issuecomment-819468549

View file

@ -16,9 +16,10 @@ defmodule Livebook.Config do
end
@doc """
Returns the runtime module to be used by default.
Returns the runtime module and `init` args used to start
the default runtime.
"""
@spec default_runtime() :: Livebook.Runtime.t()
@spec default_runtime() :: {Livebook.Runtime.t(), list()}
def default_runtime() do
Application.fetch_env!(:livebook, :default_runtime)
end
@ -141,18 +142,56 @@ defmodule Livebook.Config do
"""
def default_runtime!(env) do
if runtime = System.get_env(env) do
case runtime do
"standalone" ->
Livebook.Runtime.ElixirStandalone
default_runtime!(env, runtime)
end
end
"embedded" ->
Livebook.Runtime.Embedded
@doc """
Parses and validates default runtime within context.
"""
def default_runtime!(context, runtime) do
case runtime do
"standalone" ->
{Livebook.Runtime.ElixirStandalone, []}
other ->
abort!(
~s{expected #{env} to be either "standalone" or "embedded", got: #{inspect(other)}}
)
end
"embedded" ->
{Livebook.Runtime.Embedded, []}
"mix" ->
case mix_path(File.cwd!()) do
{:ok, path} ->
{Livebook.Runtime.MixStandalone, [path]}
:error ->
abort!(
"the current directory is not a Mix project, make sure to specify the path explicitly with mix:path"
)
end
"mix:" <> path ->
case mix_path(path) do
{:ok, path} ->
{Livebook.Runtime.MixStandalone, [path]}
:error ->
abort!(~s{"#{path}" does not point to a Mix project})
end
other ->
abort!(
~s{expected #{context} to be either "standalone", "mix[:path]" or "embedded", got: #{inspect(other)}}
)
end
end
defp mix_path(path) do
path = Path.expand(path)
mixfile = Path.join(path, "mix.exs")
if File.exists?(mixfile) do
{:ok, path}
else
:error
end
end

View file

@ -74,6 +74,21 @@ defmodule Livebook.Runtime.MixStandalone do
:ok
end
@doc """
A synchronous version of of `init_async/2`.
"""
@spec init(String.t()) :: {:ok, t()} | {:error, String.t()}
def init(project_path) do
%{ref: ref} = emitter = Livebook.Utils.Emitter.new(self())
init_async(project_path, emitter)
receive do
{:emitter, ^ref, {:ok, runtime}} -> {:ok, runtime}
{:emitter, ^ref, {:error, error}} -> {:error, error}
end
end
defp run_mix_task(task, project_path, output_emitter) do
Emitter.emit(output_emitter, "Running mix #{task}...\n")

View file

@ -379,15 +379,8 @@ defmodule Livebook.Session do
end
def handle_cast({:queue_cell_evaluation, client_pid, cell_id}, state) do
case ensure_runtime(state) do
{:ok, state} ->
operation = {:queue_cell_evaluation, client_pid, cell_id}
{:noreply, handle_operation(state, operation)}
{:error, error} ->
broadcast_error(state.session_id, "failed to setup runtime - #{error}")
{:noreply, state}
end
operation = {:queue_cell_evaluation, client_pid, cell_id}
{:noreply, handle_operation(state, operation)}
end
def handle_cast({:cancel_cell_evaluation, client_pid, cell_id}, state) do
@ -667,8 +660,34 @@ defmodule Livebook.Session do
Enum.reduce(actions, state, &handle_action(&2, &1))
end
defp handle_action(state, {:start_evaluation, cell, section}) do
start_evaluation(state, cell, section)
defp handle_action(state, :start_runtime) do
{runtime_module, args} = Livebook.Config.default_runtime()
case apply(runtime_module, :init, args) do
{:ok, runtime} ->
runtime_monitor_ref = Runtime.connect(runtime)
%{state | runtime_monitor_ref: runtime_monitor_ref}
|> handle_operation({:set_runtime, self(), runtime})
{:error, error} ->
broadcast_error(state.session_id, "failed to setup runtime - #{error}")
handle_operation(state, {:set_runtime, self(), nil})
end
end
defp handle_action(state, {:start_evaluation, cell, _section}) do
prev_ref =
state.data.notebook
|> Notebook.parent_cells_with_section(cell.id)
|> Enum.find_value(fn {cell, _} -> is_struct(cell, Cell.Elixir) && cell.id end)
file = (state.data.path || "") <> "#cell"
opts = [file: file]
Runtime.evaluate_code(state.data.runtime, cell.source, :main, cell.id, prev_ref, opts)
state
end
defp handle_action(state, {:stop_evaluation, _section}) do
@ -705,34 +724,6 @@ defmodule Livebook.Session do
Phoenix.PubSub.broadcast(Livebook.PubSub, "sessions:#{session_id}", message)
end
defp start_evaluation(state, cell, _section) do
prev_ref =
state.data.notebook
|> Notebook.parent_cells_with_section(cell.id)
|> Enum.find_value(fn {cell, _} -> is_struct(cell, Cell.Elixir) && cell.id end)
file = (state.data.path || "") <> "#cell"
opts = [file: file]
Runtime.evaluate_code(state.data.runtime, cell.source, :main, cell.id, prev_ref, opts)
state
end
# Checks if a runtime already set, and if that's not the case
# starts a new standalone one.
defp ensure_runtime(%{data: %{runtime: nil}} = state) do
with {:ok, runtime} <- Livebook.Config.default_runtime().init() do
runtime_monitor_ref = Runtime.connect(runtime)
{:ok,
%{state | runtime_monitor_ref: runtime_monitor_ref}
|> handle_operation({:set_runtime, self(), runtime})}
end
end
defp ensure_runtime(state), do: {:ok, state}
defp maybe_save_notebook(state) do
if state.data.path != nil and state.data.dirty do
content = LiveMarkdown.Export.notebook_to_markdown(state.data.notebook)

View file

@ -101,7 +101,8 @@ defmodule Livebook.Session.Data do
| {:mark_as_not_dirty, pid()}
@type action ::
{:start_evaluation, Cell.t(), Section.t()}
:start_runtime
| {:start_evaluation, Cell.t(), Section.t()}
| {:stop_evaluation, Section.t()}
| {:forget_evaluation, Cell.t(), Section.t()}
| {:broadcast_delta, pid(), Cell.t(), Delta.t()}
@ -258,6 +259,7 @@ defmodule Livebook.Session.Data do
|> with_actions()
|> queue_prerequisite_cells_evaluation(cell)
|> queue_cell_evaluation(cell, section)
|> maybe_start_runtime(data)
|> maybe_evaluate_queued()
|> wrap_ok()
else
@ -431,8 +433,7 @@ defmodule Livebook.Session.Data do
def apply_operation(data, {:set_runtime, _client_pid, runtime}) do
data
|> with_actions()
|> set!(runtime: runtime)
|> clear_evaluation()
|> set_runtime(data, runtime)
|> wrap_ok()
end
@ -616,14 +617,27 @@ defmodule Livebook.Session.Data do
|> reduce(invalidated_cells, &set_cell_info!(&1, &2.id, validity_status: :stale))
end
defp maybe_start_runtime({data, _} = data_actions, prev_data) do
if data.runtime == nil and not any_cell_queued?(prev_data) and any_cell_queued?(data) do
add_action(data_actions, :start_runtime)
else
data_actions
end
end
defp any_cell_queued?(data) do
Enum.any?(data.section_infos, fn {_section_id, info} -> info.evaluation_queue != [] end)
end
defp maybe_evaluate_queued({data, _} = data_actions) do
ongoing_evaluation? =
Enum.any?(data.notebook.sections, fn section ->
data.section_infos[section.id].evaluating_cell_id != nil
end)
if ongoing_evaluation? do
# A section is evaluating, so we don't start any new evaluation
if ongoing_evaluation? or data.runtime == nil do
# Don't tigger evaluation if there is one already,
# or if we simply don't have a runtime started yet
data_actions
else
Enum.find_value(data.notebook.sections, data_actions, fn section ->
@ -801,6 +815,16 @@ defmodule Livebook.Session.Data do
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &Map.merge(&1, attrs)))
end
defp set_runtime(data_actions, prev_data, runtime) do
{data, _} = data_actions = set!(data_actions, runtime: runtime)
if prev_data.runtime == nil and data.runtime != nil do
maybe_evaluate_queued(data_actions)
else
clear_evaluation(data_actions)
end
end
defp purge_deltas(cell_info) do
# Given client at revision X and upstream revision Y,
# we need Y - X last deltas that the client is not aware of,

View file

@ -19,15 +19,21 @@ defmodule LivebookCLI.Server do
Available options:
--cookie Sets a cookie for the app distributed node
--ip The ip address to start the web application on, defaults to 127.0.0.1
Must be a valid IPv4 or IPv6 address
--name Set a name for the app distributed node
--no-token Disable token authentication, enabled by default
If LIVEBOOK_PASSWORD is set, it takes precedence over token auth
-p, --port The port to start the web application on, defaults to 8080
--root-path The root path to use for file selection
--sname Set a short name for the app distributed node
--cookie Sets a cookie for the app distributed node
--default-runtime Sets the runtime type that is used by default when none is started
explicitly for the given notebook, defaults to standalone
Supported options:
* standalone - Elixir standalone
* mix[:path] - Mix standalone
* embedded - Embedded
--ip The ip address to start the web application on, defaults to 127.0.0.1
Must be a valid IPv4 or IPv6 address
--name Set a name for the app distributed node
--no-token Disable token authentication, enabled by default
If LIVEBOOK_PASSWORD is set, it takes precedence over token auth
-p, --port The port to start the web application on, defaults to 8080
--root-path The root path to use for file selection
--sname Set a short name for the app distributed node
The --help option can be given to print this notice.
@ -75,6 +81,7 @@ defmodule LivebookCLI.Server do
@switches [
cookie: :string,
default_runtime: :string,
ip: :string,
name: :string,
port: :integer,
@ -138,5 +145,10 @@ defmodule LivebookCLI.Server do
opts_to_config(opts, [{:livebook, :cookie, cookie} | config])
end
defp opts_to_config([{:default_runtime, default_runtime} | opts], config) do
default_runtime = Livebook.Config.default_runtime!("--default-runtime", default_runtime)
opts_to_config(opts, [{:livebook, :default_runtime, default_runtime} | config])
end
defp opts_to_config([_opt | opts], config), do: opts_to_config(opts, config)
end

View file

@ -168,7 +168,7 @@ defmodule Livebook.EvaluatorTest do
describe "request_completion_items/5" do
test "sends completion response to the given process", %{evaluator: evaluator} do
Evaluator.request_completion_items(evaluator, self(), :comp_ref, "System.ver")
assert_receive {:completion_response, :comp_ref, [%{label: "version/0"}]}
assert_receive {:completion_response, :comp_ref, [%{label: "version/0"}]}, 1_000
end
test "given evaluation reference uses its bindings and env", %{evaluator: evaluator} do
@ -181,11 +181,11 @@ defmodule Livebook.EvaluatorTest do
assert_receive {:evaluation_response, :code_1, _}
Evaluator.request_completion_items(evaluator, self(), :comp_ref, "num", :code_1)
assert_receive {:completion_response, :comp_ref, [%{label: "number"}]}
assert_receive {:completion_response, :comp_ref, [%{label: "number"}]}, 1_000
Evaluator.request_completion_items(evaluator, self(), :comp_ref, "ANSI.brigh", :code_1)
assert_receive {:completion_response, :comp_ref, [%{label: "bright/0"}]}
assert_receive {:completion_response, :comp_ref, [%{label: "bright/0"}]}, 1_000
end
end

View file

@ -2,10 +2,12 @@ defmodule Livebook.Session.DataTest do
use ExUnit.Case, async: true
alias Livebook.Session.Data
alias Livebook.{Delta, Notebook, Runtime}
alias Livebook.{Delta, Notebook}
alias Livebook.Notebook.Cell
alias Livebook.Users.User
alias Livebook.Runtime.NoopRuntime
describe "new/1" do
test "called with no arguments defaults to a blank notebook" do
empty_map = %{}
@ -127,6 +129,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -166,6 +169,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -185,6 +189,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
# Evaluate both cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}},
{:queue_cell_evaluation, self(), "c2"},
@ -241,6 +246,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
{:insert_cell, self(), "s1", 3, :elixir, "c4"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c2"},
@ -281,6 +287,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
{:insert_cell, self(), "s1", 3, :elixir, "c4"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c2"},
@ -345,6 +352,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :elixir, "c3"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c2"},
@ -369,6 +377,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :markdown, "c2"},
# Evaluate the Elixir cell
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}}
])
@ -391,6 +400,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
# Evaluate the Elixir cell
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -414,6 +424,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :markdown, "c2"},
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c3"},
@ -461,6 +472,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :elixir, "c3"},
{:insert_cell, self(), "s2", 1, :elixir, "c4"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c2"},
@ -505,6 +517,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :elixir, "c3"},
{:insert_cell, self(), "s2", 1, :elixir, "c4"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c2"},
@ -549,6 +562,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 1, :elixir, "c2"},
{:insert_cell, self(), "s3", 0, :elixir, "c3"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c2"},
@ -574,6 +588,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s2", 0, :markdown, "c2"},
# Evaluate the Elixir cell
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}}
])
@ -597,6 +612,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s2", 0, :elixir, "c2"},
# Evaluate the Elixir cell
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -624,6 +640,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s3", 0, :elixir, "c3"},
{:insert_cell, self(), "s4", 0, :markdown, "c4"},
# Evaluate cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}},
{:queue_cell_evaluation, self(), "c3"},
@ -665,6 +682,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"}
])
@ -672,7 +690,7 @@ defmodule Livebook.Session.DataTest do
assert :error = Data.apply_operation(data, operation)
end
test "marks the cell as evaluating if the corresponding section is idle" do
test "returns start runtime action if there is no runtime and this is the first evaluation" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
@ -681,6 +699,46 @@ defmodule Livebook.Session.DataTest do
operation = {:queue_cell_evaluation, self(), "c1"}
assert {:ok,
%{
cell_infos: %{"c1" => %{evaluation_status: :queued}},
section_infos: %{"s1" => %{evaluating_cell_id: nil, evaluation_queue: ["c1"]}}
}, [:start_runtime]} = Data.apply_operation(data, operation)
end
test "only queues the cell if runtime start has already been requested" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:queue_cell_evaluation, self(), "c1"}
])
operation = {:queue_cell_evaluation, self(), "c2"}
assert {:ok,
%{
cell_infos: %{
"c1" => %{evaluation_status: :queued},
"c2" => %{evaluation_status: :queued}
},
section_infos: %{
"s1" => %{evaluating_cell_id: nil, evaluation_queue: ["c1", "c2"]}
}
}, []} = Data.apply_operation(data, operation)
end
test "marks the cell as evaluating if the corresponding section is idle" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()}
])
operation = {:queue_cell_evaluation, self(), "c1"}
assert {:ok,
%{
cell_infos: %{"c1" => %{evaluation_status: :evaluating}},
@ -692,7 +750,8 @@ defmodule Livebook.Session.DataTest do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"}
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()}
])
operation = {:queue_cell_evaluation, self(), "c1"}
@ -707,6 +766,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"}
])
@ -726,6 +786,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"}
])
@ -751,6 +812,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s2", 0, :elixir, "c3"},
{:insert_cell, self(), "s2", 1, :elixir, "c4"},
# Evaluate first 2 cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}},
{:queue_cell_evaluation, self(), "c2"},
@ -795,6 +857,8 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"}
])
@ -817,6 +881,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_output, self(), "c1", "Hola"}
])
@ -840,6 +905,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_output, self(), "c1", "Hola"}
])
@ -863,6 +929,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}}
])
@ -888,6 +955,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"}
])
@ -911,6 +979,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"}
])
@ -929,6 +998,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"}
])
@ -955,6 +1025,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -977,6 +1048,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -995,12 +1067,13 @@ defmodule Livebook.Session.DataTest do
Data.apply_operation(data, operation)
end
test "if parent cells are not executed, marks them for evaluation first" do
test "if parent cells are not evaluated, marks them for evaluation first" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"}
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()}
])
operation = {:queue_cell_evaluation, self(), "c2"}
@ -1026,6 +1099,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :elixir, "c3"},
# Evaluate all cells
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}},
{:queue_cell_evaluation, self(), "c2"},
@ -1056,6 +1130,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"},
{:queue_cell_evaluation, self(), "c3"},
@ -1090,6 +1165,7 @@ defmodule Livebook.Session.DataTest do
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}}
])
@ -1106,6 +1182,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:insert_section, self(), 1, "s2"},
{:insert_cell, self(), "s2", 0, :elixir, "c3"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}},
{:queue_cell_evaluation, self(), "c2"},
@ -1134,6 +1211,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -1150,6 +1228,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"}
])
@ -1170,6 +1249,7 @@ defmodule Livebook.Session.DataTest do
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:insert_cell, self(), "s1", 2, :elixir, "c3"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"},
{:queue_cell_evaluation, self(), "c3"}
@ -1649,6 +1729,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :input, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c2"},
{:add_cell_evaluation_response, self(), "c2", {:ok, [1, 2, 3]}}
])
@ -1667,9 +1748,7 @@ defmodule Livebook.Session.DataTest do
test "updates data with the given runtime" do
data = Data.new()
{:ok, runtime} = Runtime.Embedded.init()
Runtime.connect(runtime)
runtime = NoopRuntime.new()
operation = {:set_runtime, self(), runtime}
assert {:ok, %{runtime: ^runtime}, []} = Data.apply_operation(data, operation)
@ -1682,6 +1761,7 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:insert_cell, self(), "s1", 1, :elixir, "c2"},
{:set_runtime, self(), NoopRuntime.new()},
{:queue_cell_evaluation, self(), "c1"},
{:queue_cell_evaluation, self(), "c2"},
# Second section with evaluating and queued cells
@ -1692,9 +1772,7 @@ defmodule Livebook.Session.DataTest do
{:queue_cell_evaluation, self(), "c4"}
])
{:ok, runtime} = Runtime.Embedded.init()
Runtime.connect(runtime)
runtime = NoopRuntime.new()
operation = {:set_runtime, self(), runtime}
assert {:ok,
@ -1711,6 +1789,30 @@ defmodule Livebook.Session.DataTest do
}
}, []} = Data.apply_operation(data, operation)
end
test "starts evaluation if there was no runtime before and there is now" do
data =
data_after_operations!([
{:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:queue_cell_evaluation, self(), "c1"}
])
runtime = NoopRuntime.new()
operation = {:set_runtime, self(), runtime}
assert {:ok,
%{
cell_infos: %{
"c1" => %{evaluation_status: :evaluating}
},
section_infos: %{
"s1" => %{evaluating_cell_id: "c1", evaluation_queue: []}
}
},
[{:start_evaluation, %{id: "c1"}, %{id: "s1"}}]} =
Data.apply_operation(data, operation)
end
end
describe "apply_operation/2 given :set_path" do

View file

@ -0,0 +1,19 @@
defmodule Livebook.Runtime.NoopRuntime do
@moduledoc false
# A runtime that doesn't do any actual evaluation,
# thus not requiring any underlying resources.
defstruct []
def new(), do: %__MODULE__{}
defimpl Livebook.Runtime do
def connect(_), do: :ok
def disconnect(_), do: :ok
def evaluate_code(_, _, _, _, _, _ \\ []), do: :ok
def forget_evaluation(_, _, _), do: :ok
def drop_container(_, _), do: :ok
def request_completion_items(_, _, _, _, _, _), do: :ok
end
end