Add VegaLite widget integration (#306)

* Add support for LiveWidget.VegaLite

* LiveWidget -> Kino

* Show an error when rendering unsupported Kino widget

* Match on Kino.Widget

* Add catch-all for unknown outputs
This commit is contained in:
Jonatan Kłosko 2021-05-31 22:48:05 +02:00 committed by GitHub
parent d6c9ab1783
commit ce7adef7e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 211 additions and 58 deletions

View file

@ -1,13 +1,20 @@
import * as vega from "vega";
import vegaEmbed from "vega-embed"; import vegaEmbed from "vega-embed";
import { getAttributeOrThrow } from "../lib/attribute"; import { getAttributeOrThrow } from "../lib/attribute";
// See https://github.com/vega/vega-lite/blob/b61b13c2cbd4ecde0448544aff6cdaea721fd22a/src/compile/data/assemble.ts#L228-L231
const DEFAULT_DATASET_NAME = "source_0";
/** /**
* A hook used to render graphics according to the given * A hook used to render graphics according to the given
* Vega-Lite specification. * Vega-Lite specification.
* *
* The hook expects a `vega_lite:<id>` event with `{ spec }` payload, * The hook expects a `vega_lite:<id>:init` event with `{ spec }` payload,
* where `spec` is the graphic definition as an object. * where `spec` is the graphic definition as an object.
* *
* Later `vega_lite:<id>:push` events may be sent with `{ data, dataset, window }` payload,
* to dynamically update the underlying data.
*
* Configuration: * Configuration:
* *
* * `data-id` - plot id * * `data-id` - plot id
@ -18,19 +25,45 @@ const VegaLite = {
this.props = getProps(this); this.props = getProps(this);
this.state = { this.state = {
container: null, container: null,
viewPromise: null,
}; };
this.state.container = document.createElement("div"); this.state.container = document.createElement("div");
this.el.appendChild(this.state.container); this.el.appendChild(this.state.container);
this.handleEvent(`vega_lite:${this.props.id}`, ({ spec }) => { this.handleEvent(`vega_lite:${this.props.id}:init`, ({ spec }) => {
vegaEmbed(this.state.container, spec, {}); if (!spec.data) {
spec.data = { values: [] };
}
this.state.viewPromise = vegaEmbed(this.state.container, spec, {}).then(
(result) => result.view
);
}); });
this.handleEvent(
`vega_lite:${this.props.id}:push`,
({ data, dataset, window }) => {
dataset = dataset || DEFAULT_DATASET_NAME;
this.state.viewPromise.then((view) => {
const currentData = view.data(dataset);
const changeset = buildChangeset(currentData, data, window);
view.change(dataset, changeset).run();
});
}
);
}, },
updated() { updated() {
this.props = getProps(this); this.props = getProps(this);
}, },
destroyed() {
if (this.state.viewPromise) {
this.state.viewPromise.then((view) => view.finalize());
}
},
}; };
function getProps(hook) { function getProps(hook) {
@ -39,4 +72,18 @@ function getProps(hook) {
}; };
} }
function buildChangeset(currentData, newData, window) {
if (window === 0) {
return vega.changeset().remove(currentData);
} else if (window) {
const toInsert = newData.slice(-window);
const freeSpace = Math.max(window - toInsert.length, 0);
const toRemove = currentData.slice(0, -freeSpace);
return vega.changeset().remove(toRemove).insert(toInsert);
} else {
return vega.changeset().insert(newData);
}
}
export default VegaLite; export default VegaLite;

View file

@ -46,8 +46,8 @@ defmodule Livebook.Evaluator do
Options: Options:
* `formatter` - a module implementing the `Livebook.Evaluator.Formatter` behaviour, * `formatter` - a module implementing the `Livebook.Evaluator.Formatter` behaviour,
used for transforming evaluation response before it's sent to the client used for transforming evaluation response before it's sent to the client
""" """
def start_link(opts \\ []) do def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts) GenServer.start_link(__MODULE__, opts)
@ -102,7 +102,7 @@ defmodule Livebook.Evaluator do
def init(opts) do def init(opts) do
formatter = Keyword.get(opts, :formatter, Evaluator.IdentityFormatter) formatter = Keyword.get(opts, :formatter, Evaluator.IdentityFormatter)
{:ok, io_proxy} = Evaluator.IOProxy.start_link() {:ok, io_proxy} = Evaluator.IOProxy.start_link(formatter: formatter)
# Use the dedicated IO device as the group leader, # Use the dedicated IO device as the group leader,
# so that it handles all :stdio operations. # so that it handles all :stdio operations.
@ -165,7 +165,7 @@ defmodule Livebook.Evaluator do
end end
defp send_evaluation_response(send_to, ref, evaluation_response, formatter) do defp send_evaluation_response(send_to, ref, evaluation_response, formatter) do
response = formatter.format(evaluation_response) response = formatter.format_response(evaluation_response)
send(send_to, {:evaluation_response, ref, response}) send(send_to, {:evaluation_response, ref, response})
end end

View file

@ -6,36 +6,49 @@ defmodule Livebook.Evaluator.DefaultFormatter do
@behaviour Livebook.Evaluator.Formatter @behaviour Livebook.Evaluator.Formatter
@impl true @impl true
def format({:ok, :"do not show this result in output"}) do def format_output(string) when is_binary(string), do: string
def format_output(other), do: format_value(other)
@impl true
def format_response({:ok, :"do not show this result in output"}) do
# Functions in the `IEx.Helpers` module return this specific value # Functions in the `IEx.Helpers` module return this specific value
# to indicate no result should be printed in the iex shell, # to indicate no result should be printed in the iex shell,
# so we respect that as well. # so we respect that as well.
:ignored :ignored
end end
def format({:ok, {:module, _, _, _} = value}) do def format_response({:ok, {:module, _, _, _} = value}) do
inspected = inspect(value, inspect_opts(limit: 10)) inspected = inspect(value, inspect_opts(limit: 10))
{:inspect, inspected} {:inspect, inspected}
end end
def format_response({:ok, value}) do
format_value(value)
end
def format_response({:error, kind, error, stacktrace}) do
formatted = Exception.format(kind, error, stacktrace)
{:error, formatted}
end
# ---
@compile {:no_warn_undefined, {VegaLite, :to_spec, 1}} @compile {:no_warn_undefined, {VegaLite, :to_spec, 1}}
def format({:ok, value}) do defp format_value(value) do
cond do cond do
is_struct(value, VegaLite) and function_exported?(VegaLite, :to_spec, 1) -> is_struct(value, VegaLite) and function_exported?(VegaLite, :to_spec, 1) ->
{:vega_lite_spec, VegaLite.to_spec(value)} {:vega_lite_spec, VegaLite.to_spec(value)}
is_struct(value, Kino.Widget) ->
{:kino_widget, value.type, value.pid}
true -> true ->
inspected = inspect(value, inspect_opts()) inspected = inspect(value, inspect_opts())
{:inspect, inspected} {:inspect, inspected}
end end
end end
def format({:error, kind, error, stacktrace}) do
formatted = Exception.format(kind, error, stacktrace)
{:error, formatted}
end
defp inspect_opts(opts \\ []) do defp inspect_opts(opts \\ []) do
default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()] default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()]
Keyword.merge(default_opts, opts) Keyword.merge(default_opts, opts)

View file

@ -9,11 +9,20 @@ defmodule Livebook.Evaluator.Formatter do
# we would unnecessarily send a lot of data. # we would unnecessarily send a lot of data.
# By defining a custom formatter the client can instruct # By defining a custom formatter the client can instruct
# the `Evaluator` to send already transformed data. # the `Evaluator` to send already transformed data.
#
# Additionally if the results rely on external package installed
# in the runtime node, then formatting anywhere else wouldn't be accurate,
# for example using `inspect` on an external struct.
alias Livebook.Evaluator alias Livebook.Evaluator
@doc """
Transforms arbitrary evaluation output, usually binary.
"""
@callback format_output(term()) :: term()
@doc """ @doc """
Transforms the evaluation response. Transforms the evaluation response.
""" """
@callback format(Evaluator.evaluation_response()) :: term() @callback format_response(Evaluator.evaluation_response()) :: term()
end end

View file

@ -1,10 +1,13 @@
defmodule Livebook.Evaluator.IdentityFormatter do defmodule Livebook.Evaluator.IdentityFormatter do
@moduledoc false @moduledoc false
# The default formatter leaving the response unchanged. # The default formatter leaving the output unchanged.
@behaviour Livebook.Evaluator.Formatter @behaviour Livebook.Evaluator.Formatter
@impl true @impl true
def format(evaluation_response), do: evaluation_response def format_output(output), do: output
@impl true
def format_response(evaluation_response), do: evaluation_response
end end

View file

@ -23,6 +23,11 @@ defmodule Livebook.Evaluator.IOProxy do
Starts the IO device process. Starts the IO device process.
Make sure to use `configure/3` to actually proxy the requests. Make sure to use `configure/3` to actually proxy the requests.
Options:
* `formatter` - a module implementing the `Livebook.Evaluator.Formatter` behaviour,
used for transforming outputs before they are sent to the client
""" """
@spec start_link() :: GenServer.on_start() @spec start_link() :: GenServer.on_start()
def start_link(opts \\ []) do def start_link(opts \\ []) do
@ -38,7 +43,7 @@ defmodule Livebook.Evaluator.IOProxy do
The possible messages are: The possible messages are:
* `{:evaluation_stdout, ref, string}` - for output requests, * `{:evaluation_output, ref, string}` - for output requests,
where `ref` is the given evaluation reference and `string` is the output. where `ref` is the given evaluation reference and `string` is the output.
""" """
@spec configure(pid(), pid(), Evaluator.ref()) :: :ok @spec configure(pid(), pid(), Evaluator.ref()) :: :ok
@ -57,8 +62,9 @@ defmodule Livebook.Evaluator.IOProxy do
## Callbacks ## Callbacks
@impl true @impl true
def init(_opts) do def init(opts) do
{:ok, %{encoding: :unicode, target: nil, ref: nil, buffer: []}} formatter = Keyword.get(opts, :formatter, Evaluator.IdentityFormatter)
{:ok, %{encoding: :unicode, target: nil, ref: nil, buffer: [], formatter: formatter}}
end end
@impl true @impl true
@ -150,6 +156,16 @@ defmodule Livebook.Evaluator.IOProxy do
io_requests(reqs, {:ok, state}) io_requests(reqs, {:ok, state})
end end
# Livebook custom request type, handled in a special manner
# by IOProxy and safely failing for any other IO device
# (resulting in the {:error, :request} response).
defp io_request({:livebook_put_term, term}, state) do
state = flush_buffer(state)
formatted_term = state.formatter.format_output(term)
send(state.target, {:evaluation_output, state.ref, formatted_term})
{:ok, state}
end
defp io_request(_, state) do defp io_request(_, state) do
{{:error, :request}, state} {{:error, :request}, state}
end end
@ -186,7 +202,8 @@ defmodule Livebook.Evaluator.IOProxy do
string = state.buffer |> Enum.reverse() |> Enum.join() string = state.buffer |> Enum.reverse() |> Enum.join()
if state.target != nil and string != "" do if state.target != nil and string != "" do
send(state.target, {:evaluation_stdout, state.ref, string}) formatted_string = state.formatter.format_output(string)
send(state.target, {:evaluation_output, state.ref, formatted_string})
end end
%{state | buffer: []} %{state | buffer: []}

View file

@ -52,8 +52,8 @@ defprotocol Livebook.Runtime do
Evaluation outputs are send to the connected runtime owner. Evaluation outputs are send to the connected runtime owner.
The messages should be of the form: The messages should be of the form:
* `{:evaluation_stdout, ref, string}` - output captured during evaluation * `{:evaluation_output, ref, output}` - output captured during evaluation
* `{:evaluation_response, ref, response}` - final result of the evaluation * `{:evaluation_response, ref, output}` - final result of the evaluation
If the evaluation state within a container is lost (e.g. a process goes down), If the evaluation state within a container is lost (e.g. a process goes down),
the runtime can send `{:container_down, container_ref, message}` to notify the owner. the runtime can send `{:container_down, container_ref, message}` to notify the owner.

View file

@ -484,8 +484,8 @@ defmodule Livebook.Session do
{:noreply, state} {:noreply, state}
end end
def handle_info({:evaluation_stdout, cell_id, string}, state) do def handle_info({:evaluation_output, cell_id, string}, state) do
operation = {:add_cell_evaluation_stdout, self(), cell_id, string} operation = {:add_cell_evaluation_output, self(), cell_id, string}
{:noreply, handle_operation(state, operation)} {:noreply, handle_operation(state, operation)}
end end

View file

@ -27,7 +27,7 @@ defmodule Livebook.Session.Data do
:users_map :users_map
] ]
alias Livebook.{Notebook, Evaluator, Delta, Runtime, JSInterop} alias Livebook.{Notebook, Delta, Runtime, JSInterop}
alias Livebook.Users.User alias Livebook.Users.User
alias Livebook.Notebook.{Cell, Section} alias Livebook.Notebook.{Cell, Section}
@ -84,8 +84,8 @@ defmodule Livebook.Session.Data do
| {:move_cell, pid(), Cell.id(), offset :: integer()} | {:move_cell, pid(), Cell.id(), offset :: integer()}
| {:move_section, pid(), Section.id(), offset :: integer()} | {:move_section, pid(), Section.id(), offset :: integer()}
| {:queue_cell_evaluation, pid(), Cell.id()} | {:queue_cell_evaluation, pid(), Cell.id()}
| {:add_cell_evaluation_stdout, pid(), Cell.id(), String.t()} | {:add_cell_evaluation_output, pid(), Cell.id(), term()}
| {:add_cell_evaluation_response, pid(), Cell.id(), Evaluator.evaluation_response()} | {:add_cell_evaluation_response, pid(), Cell.id(), term()}
| {:reflect_evaluation_failure, pid()} | {:reflect_evaluation_failure, pid()}
| {:cancel_cell_evaluation, pid(), Cell.id()} | {:cancel_cell_evaluation, pid(), Cell.id()}
| {:set_notebook_name, pid(), String.t()} | {:set_notebook_name, pid(), String.t()}
@ -265,23 +265,23 @@ defmodule Livebook.Session.Data do
end end
end end
def apply_operation(data, {:add_cell_evaluation_stdout, _client_pid, id, string}) do def apply_operation(data, {:add_cell_evaluation_output, _client_pid, id, output}) do
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, id) do with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, id) do
data data
|> with_actions() |> with_actions()
|> add_cell_evaluation_stdout(cell, string) |> add_cell_evaluation_output(cell, output)
|> wrap_ok() |> wrap_ok()
else else
_ -> :error _ -> :error
end end
end end
def apply_operation(data, {:add_cell_evaluation_response, _client_pid, id, response}) do def apply_operation(data, {:add_cell_evaluation_response, _client_pid, id, output}) do
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, id), with {:ok, cell, section} <- Notebook.fetch_cell_and_section(data.notebook, id),
:evaluating <- data.cell_infos[cell.id].evaluation_status do :evaluating <- data.cell_infos[cell.id].evaluation_status do
data data
|> with_actions() |> with_actions()
|> add_cell_evaluation_response(cell, response) |> add_cell_evaluation_response(cell, output)
|> finish_cell_evaluation(cell, section) |> finish_cell_evaluation(cell, section)
|> mark_dependent_cells_as_stale(cell) |> mark_dependent_cells_as_stale(cell)
|> maybe_evaluate_queued() |> maybe_evaluate_queued()
@ -532,22 +532,22 @@ defmodule Livebook.Session.Data do
|> set_cell_info!(cell.id, evaluation_status: :ready) |> set_cell_info!(cell.id, evaluation_status: :ready)
end end
defp add_cell_evaluation_stdout({data, _} = data_actions, cell, string) do defp add_cell_evaluation_output({data, _} = data_actions, cell, output) do
data_actions data_actions
|> set!( |> set!(
notebook: notebook:
Notebook.update_cell(data.notebook, cell.id, fn cell -> Notebook.update_cell(data.notebook, cell.id, fn cell ->
%{cell | outputs: add_output(cell.outputs, string)} %{cell | outputs: add_output(cell.outputs, output)}
end) end)
) )
end end
defp add_cell_evaluation_response({data, _} = data_actions, cell, response) do defp add_cell_evaluation_response({data, _} = data_actions, cell, output) do
data_actions data_actions
|> set!( |> set!(
notebook: notebook:
Notebook.update_cell(data.notebook, cell.id, fn cell -> Notebook.update_cell(data.notebook, cell.id, fn cell ->
%{cell | outputs: add_output(cell.outputs, response)} %{cell | outputs: add_output(cell.outputs, output)}
end) end)
) )
end end

View file

@ -0,0 +1,32 @@
defmodule LivebookWeb.Kino.VegaLiteLive do
use LivebookWeb, :live_view
@impl true
def mount(_params, %{"pid" => pid, "id" => id}, socket) do
send(pid, {:connect, self()})
{:ok, assign(socket, id: id)}
end
@impl true
def render(assigns) do
~L"""
<div id="vega-lite-<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>">
</div>
"""
end
@impl true
def handle_info({:connect_reply, %{spec: spec}}, socket) do
{:noreply, push_event(socket, "vega_lite:#{socket.assigns.id}:init", %{"spec" => spec})}
end
def handle_info({:push, %{data: data, dataset: dataset, window: window}}, socket) do
{:noreply,
push_event(socket, "vega_lite:#{socket.assigns.id}:push", %{
"data" => data,
"dataset" => dataset,
"window" => window
})}
end
end

View file

@ -228,7 +228,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
<div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200 font-editor"> <div class="flex flex-col rounded-lg border border-gray-200 divide-y divide-gray-200 font-editor">
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %> <%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<div class="p-4 max-w-full overflow-y-auto tiny-scrollbar"> <div class="p-4 max-w-full overflow-y-auto tiny-scrollbar">
<%= render_output(socket, output, "#{@cell_view.id}-output#{index}") %> <%= render_output(socket, output, "cell-#{@cell_view.id}-output#{index}") %>
</div> </div>
<% end %> <% end %>
</div> </div>
@ -270,11 +270,35 @@ defmodule LivebookWeb.SessionLive.CellComponent do
live_component(socket, LivebookWeb.SessionLive.VegaLiteComponent, id: id, spec: spec) live_component(socket, LivebookWeb.SessionLive.VegaLiteComponent, id: id, spec: spec)
end end
defp render_output(socket, {:kino_widget, :vega_lite, pid}, id) do
live_render(socket, LivebookWeb.Kino.VegaLiteLive,
id: id,
session: %{"id" => id, "pid" => pid}
)
end
defp render_output(_socket, {:kino_widget, type, _pid}, _id) do
render_error_message_output("""
Got unsupported Kino widget type: #{inspect(type)}, if that's a new widget
make usre to update Livebook to the latest version
""")
end
defp render_output(_socket, {:error, formatted}, _id) do defp render_output(_socket, {:error, formatted}, _id) do
assigns = %{formatted: formatted} render_error_message_output(formatted)
end
defp render_output(_socket, output, _id) do
# Above we cover all possible outputs from DefaultFormatter,
# but this is helpful in development when adding new output types.
render_error_message_output("Unknown output type: #{inspect(output)}")
end
defp render_error_message_output(message) do
assigns = %{message: message}
~L""" ~L"""
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @formatted %></div> <div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div>
""" """
end end

View file

@ -9,13 +9,13 @@ defmodule LivebookWeb.SessionLive.VegaLiteComponent do
@impl true @impl true
def update(assigns, socket) do def update(assigns, socket) do
socket = assign(socket, id: assigns.id) socket = assign(socket, id: assigns.id)
{:ok, push_event(socket, "vega_lite:#{socket.assigns.id}", %{"spec" => assigns.spec})} {:ok, push_event(socket, "vega_lite:#{socket.assigns.id}:init", %{"spec" => assigns.spec})}
end end
@impl true @impl true
def render(assigns) do def render(assigns) do
~L""" ~L"""
<div id="<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>"> <div id="vega-lite-<%= @id %>" phx-hook="VegaLite" phx-update="ignore" data-id="<%= @id %>">
</div> </div>
""" """
end end

View file

@ -12,17 +12,17 @@ defmodule Livebook.Evaluator.IOProxyTest do
describe ":stdio interoperability" do describe ":stdio interoperability" do
test "IO.puts", %{io: io} do test "IO.puts", %{io: io} do
IO.puts(io, "hey") IO.puts(io, "hey")
assert_receive {:evaluation_stdout, :ref, "hey\n"} assert_receive {:evaluation_output, :ref, "hey\n"}
end end
test "IO.write", %{io: io} do test "IO.write", %{io: io} do
IO.write(io, "hey") IO.write(io, "hey")
assert_receive {:evaluation_stdout, :ref, "hey"} assert_receive {:evaluation_output, :ref, "hey"}
end end
test "IO.inspect", %{io: io} do test "IO.inspect", %{io: io} do
IO.inspect(io, %{}, []) IO.inspect(io, %{}, [])
assert_receive {:evaluation_stdout, :ref, "%{}\n"} assert_receive {:evaluation_output, :ref, "%{}\n"}
end end
test "IO.read", %{io: io} do test "IO.read", %{io: io} do
@ -37,18 +37,26 @@ defmodule Livebook.Evaluator.IOProxyTest do
test "buffers rapid output", %{io: io} do test "buffers rapid output", %{io: io} do
IO.puts(io, "hey") IO.puts(io, "hey")
IO.puts(io, "hey") IO.puts(io, "hey")
assert_receive {:evaluation_stdout, :ref, "hey\nhey\n"} assert_receive {:evaluation_output, :ref, "hey\nhey\n"}
end end
test "respects CR as line cleaner", %{io: io} do test "respects CR as line cleaner", %{io: io} do
IO.write(io, "hey") IO.write(io, "hey")
IO.write(io, "\roverride\r") IO.write(io, "\roverride\r")
assert_receive {:evaluation_stdout, :ref, "\roverride\r"} assert_receive {:evaluation_output, :ref, "\roverride\r"}
end end
test "flush/1 synchronously sends buffer contents", %{io: io} do test "flush/1 synchronously sends buffer contents", %{io: io} do
IO.puts(io, "hey") IO.puts(io, "hey")
IOProxy.flush(io) IOProxy.flush(io)
assert_received {:evaluation_stdout, :ref, "hey\n"} assert_received {:evaluation_output, :ref, "hey\n"}
end
test "supports special livebook request type", %{io: io} do
ref = make_ref()
send(io, {:io_request, self(), ref, {:livebook_put_term, [1, 2, 3]}})
assert_receive {:io_reply, ^ref, :ok}
assert_received {:evaluation_output, :ref, [1, 2, 3]}
end end
end end

View file

@ -52,7 +52,7 @@ defmodule Livebook.EvaluatorTest do
test "captures standard output and sends it to the caller", %{evaluator: evaluator} do test "captures standard output and sends it to the caller", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, self(), ~s{IO.puts("hey")}, :code_1) Evaluator.evaluate_code(evaluator, self(), ~s{IO.puts("hey")}, :code_1)
assert_receive {:evaluation_stdout, :code_1, "hey\n"} assert_receive {:evaluation_output, :code_1, "hey\n"}
end end
test "using standard input results in an immediate error", %{evaluator: evaluator} do test "using standard input results in an immediate error", %{evaluator: evaluator} do

View file

@ -63,7 +63,7 @@ defmodule Livebook.Runtime.ErlDist.ManagerTest do
Manager.evaluate_code(node(), ~s{IO.puts(:stderr, "error")}, :container1, :evaluation1, nil) Manager.evaluate_code(node(), ~s{IO.puts(:stderr, "error")}, :container1, :evaluation1, nil)
assert_receive {:evaluation_stdout, :evaluation1, "error\n"} assert_receive {:evaluation_output, :evaluation1, "error\n"}
Manager.stop(node()) Manager.stop(node())
end end
@ -80,7 +80,7 @@ defmodule Livebook.Runtime.ErlDist.ManagerTest do
Manager.evaluate_code(node(), code, :container1, :evaluation1, nil) Manager.evaluate_code(node(), code, :container1, :evaluation1, nil)
assert_receive {:evaluation_stdout, :evaluation1, log_message} assert_receive {:evaluation_output, :evaluation1, log_message}
assert log_message =~ "[error] hey" assert log_message =~ "[error] hey"
Manager.stop(node()) Manager.stop(node())

View file

@ -788,7 +788,7 @@ defmodule Livebook.Session.DataTest do
end end
end end
describe "apply_operation/2 given :add_cell_evaluation_stdout" do describe "apply_operation/2 given :add_cell_evaluation_output" do
test "updates the cell outputs" do test "updates the cell outputs" do
data = data =
data_after_operations!([ data_after_operations!([
@ -797,7 +797,7 @@ defmodule Livebook.Session.DataTest do
{:queue_cell_evaluation, self(), "c1"} {:queue_cell_evaluation, self(), "c1"}
]) ])
operation = {:add_cell_evaluation_stdout, self(), "c1", "Hello!"} operation = {:add_cell_evaluation_output, self(), "c1", "Hello!"}
assert {:ok, assert {:ok,
%{ %{
@ -817,10 +817,10 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"}, {:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"}, {:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:queue_cell_evaluation, self(), "c1"}, {:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_stdout, self(), "c1", "Hola"} {:add_cell_evaluation_output, self(), "c1", "Hola"}
]) ])
operation = {:add_cell_evaluation_stdout, self(), "c1", " amigo!"} operation = {:add_cell_evaluation_output, self(), "c1", " amigo!"}
assert {:ok, assert {:ok,
%{ %{
@ -840,10 +840,10 @@ defmodule Livebook.Session.DataTest do
{:insert_section, self(), 0, "s1"}, {:insert_section, self(), 0, "s1"},
{:insert_cell, self(), "s1", 0, :elixir, "c1"}, {:insert_cell, self(), "s1", 0, :elixir, "c1"},
{:queue_cell_evaluation, self(), "c1"}, {:queue_cell_evaluation, self(), "c1"},
{:add_cell_evaluation_stdout, self(), "c1", "Hola"} {:add_cell_evaluation_output, self(), "c1", "Hola"}
]) ])
operation = {:add_cell_evaluation_stdout, self(), "c1", "\ramigo!\r"} operation = {:add_cell_evaluation_output, self(), "c1", "\ramigo!\r"}
assert {:ok, assert {:ok,
%{ %{
@ -866,7 +866,7 @@ defmodule Livebook.Session.DataTest do
{:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}} {:add_cell_evaluation_response, self(), "c1", {:ok, [1, 2, 3]}}
]) ])
operation = {:add_cell_evaluation_stdout, self(), "c1", "Hello!"} operation = {:add_cell_evaluation_output, self(), "c1", "Hello!"}
assert {:ok, assert {:ok,
%{ %{