Restructure output format (#2179)

This commit is contained in:
Jonatan Kłosko 2023-08-23 23:25:04 +02:00 committed by GitHub
parent f2795c7067
commit da9cc6643b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1132 additions and 688 deletions

View file

@ -27,7 +27,7 @@ defmodule Livebook.LiveMarkdown.Export do
defp collect_js_output_data(notebook) do
for section <- notebook.sections,
%{outputs: outputs} <- section.cells,
{_idx, {:js, %{js_view: %{ref: ref, pid: pid}, export: %{}}}} <- outputs do
{_idx, %{type: :js, js_view: %{ref: ref, pid: pid}, export: %{}}} <- outputs do
Task.async(fn ->
{ref, get_js_output_data(pid, ref)}
end)
@ -212,7 +212,7 @@ defmodule Livebook.LiveMarkdown.Export do
|> Enum.intersperse("\n\n")
end
defp render_output({:terminal_text, text, %{}}, _ctx) do
defp render_output(%{type: :terminal_text, text: text}, _ctx) do
text = String.replace_suffix(text, "\n", "")
delimiter = MarkdownHelpers.code_block_delimiter(text)
text = strip_ansi(text)
@ -222,7 +222,7 @@ defmodule Livebook.LiveMarkdown.Export do
end
defp render_output(
{:js, %{export: %{info_string: info_string, key: key}, js_view: %{ref: ref}}},
%{type: :js, export: %{info_string: info_string, key: key}, js_view: %{ref: ref}},
ctx
)
when is_binary(info_string) do
@ -241,7 +241,7 @@ defmodule Livebook.LiveMarkdown.Export do
end
end
defp render_output({:tabs, outputs, _info}, ctx) do
defp render_output(%{type: :tabs, outputs: outputs}, ctx) do
Enum.find_value(outputs, :ignored, fn {_idx, output} ->
case render_output(output, ctx) do
:ignored -> nil
@ -250,7 +250,7 @@ defmodule Livebook.LiveMarkdown.Export do
end)
end
defp render_output({:grid, outputs, _info}, ctx) do
defp render_output(%{type: :grid, outputs: outputs}, ctx) do
outputs
|> Enum.map(fn {_idx, output} -> render_output(output, ctx) end)
|> Enum.reject(&(&1 == :ignored))

View file

@ -195,7 +195,7 @@ defmodule Livebook.LiveMarkdown.Import do
[{"pre", _, [{"code", [{"class", "output"}], [output], %{}}], %{}} | ast],
outputs
) do
take_outputs(ast, [{:terminal_text, output, %{chunk: false}} | outputs])
take_outputs(ast, [%{type: :terminal_text, text: output, chunk: false} | outputs])
end
defp take_outputs(
@ -206,7 +206,7 @@ defmodule Livebook.LiveMarkdown.Import do
],
outputs
) do
take_outputs(ast, [{:terminal_text, output, %{chunk: false}} | outputs])
take_outputs(ast, [%{type: :terminal_text, text: output, chunk: false} | outputs])
end
# Ignore other exported outputs

View file

@ -641,11 +641,11 @@ defmodule Livebook.Notebook do
defp find_assets_info_in_outputs(outputs, hash) do
Enum.find_value(outputs, fn
{_idx, {:js, %{js_view: %{assets: %{hash: ^hash} = assets_info}}}} ->
{_idx, %{type: :js, js_view: %{assets: %{hash: ^hash} = assets_info}}} ->
assets_info
{_idx, {type, outputs, _}} when type in [:frame, :tabs, :grid] ->
find_assets_info_in_outputs(outputs, hash)
{_idx, output} when output.type in [:frame, :tabs, :grid] ->
find_assets_info_in_outputs(output.outputs, hash)
_ ->
nil
@ -669,28 +669,15 @@ defmodule Livebook.Notebook do
Automatically merges terminal outputs and updates frames.
"""
@spec add_cell_output(t(), Cell.id(), Livebook.Runtime.output()) :: t()
def add_cell_output(notebook, cell_id, output)
# Map legacy outputs
def add_cell_output(notebook, cell_id, {:text, text}),
do: add_cell_output(notebook, cell_id, {:terminal_text, text, %{chunk: false}})
def add_cell_output(notebook, cell_id, {:plain_text, text}),
do: add_cell_output(notebook, cell_id, {:plain_text, text, %{chunk: false}})
def add_cell_output(notebook, cell_id, {:markdown, text}),
do: add_cell_output(notebook, cell_id, {:markdown, text, %{chunk: false}})
def add_cell_output(notebook, cell_id, output) do
{notebook, counter} = do_add_cell_output(notebook, cell_id, notebook.output_counter, output)
%{notebook | output_counter: counter}
end
defp do_add_cell_output(notebook, _cell_id, counter, {:frame, _outputs, %{type: type}} = frame)
when type != :default do
defp do_add_cell_output(notebook, _cell_id, counter, %{type: :frame_update} = frame_update) do
update_reduce_cells(notebook, counter, fn
%{outputs: _} = cell, counter ->
{outputs, counter} = update_frames(cell.outputs, counter, frame)
{outputs, counter} = update_frames(cell.outputs, counter, frame_update)
{%{cell | outputs: outputs}, counter}
cell, counter ->
@ -709,17 +696,17 @@ defmodule Livebook.Notebook do
{notebook, counter}
end
defp update_frames(outputs, counter, {:frame, new_outputs, %{ref: ref, type: type}} = frame) do
defp update_frames(outputs, counter, %{ref: ref} = frame_update) do
Enum.map_reduce(outputs, counter, fn
{idx, {:frame, outputs, %{ref: ^ref} = info}}, counter ->
{idx, %{type: :frame, outputs: outputs, ref: ^ref} = frame}, counter ->
{update_type, new_outputs} = frame_update.update
{new_outputs, counter} = index_outputs(new_outputs, counter)
output = {idx, {:frame, apply_frame_update(outputs, new_outputs, type), info}}
{output, counter}
outputs = apply_frame_update(outputs, new_outputs, update_type)
{{idx, %{frame | outputs: outputs}}, counter}
{idx, {type, outputs, info}}, counter when type in [:frame, :tabs, :grid] ->
{outputs, counter} = update_frames(outputs, counter, frame)
output = {idx, {type, outputs, info}}
{output, counter}
{idx, output}, counter when output.type in [:frame, :tabs, :grid] ->
{outputs, counter} = update_frames(output.outputs, counter, frame_update)
{{idx, %{output | outputs: outputs}}, counter}
output, counter ->
{output, counter}
@ -734,43 +721,40 @@ defmodule Livebook.Notebook do
Enum.reduce(Enum.reverse(new_outputs), outputs, &add_output(&2, &1))
end
defp add_output(outputs, {_idx, :ignored}), do: outputs
defp add_output(outputs, {_idx, %{type: :ignored}}), do: outputs
# Session clients prune rendered chunks, we only keep add the new one
# Session clients prune rendered chunks, we only add the new one
defp add_output(
[{idx, {type, :__pruned__, %{chunk: true} = info}} | tail],
{_idx, {type, text, %{chunk: true}}}
[{idx, %{type: type, chunk: true, text: :__pruned__} = output} | tail],
{_idx, %{type: type, chunk: true, text: text}}
)
when type in [:terminal_text, :plain_text, :markdown] do
[{idx, {type, text, info}} | tail]
[{idx, %{output | text: text}} | tail]
end
# Session server keeps all outputs, so we merge consecutive chunks
defp add_output(
[{idx, {:terminal_text, text, %{chunk: true} = info}} | tail],
{_idx, {:terminal_text, cont, %{chunk: true}}}
[{idx, %{type: :terminal_text, chunk: true, text: text} = output} | tail],
{_idx, %{type: :terminal_text, chunk: true, text: cont}}
) do
[{idx, {:terminal_text, normalize_terminal_text(text <> cont), info}} | tail]
[{idx, %{output | text: normalize_terminal_text(text <> cont)}} | tail]
end
defp add_output(outputs, {idx, {:terminal_text, text, info}}) do
[{idx, {:terminal_text, normalize_terminal_text(text), info}} | outputs]
defp add_output(outputs, {idx, %{type: :terminal_text, text: text} = output}) do
[{idx, %{output | text: normalize_terminal_text(text)}} | outputs]
end
defp add_output(
[{idx, {type, text, %{chunk: true} = info}} | tail],
{_idx, {type, cont, %{chunk: true}}}
[{idx, %{type: type, chunk: true, text: text} = output} | tail],
{_idx, %{type: type, chunk: true, text: cont}}
)
when type in [:plain_text, :markdown] do
[{idx, {type, normalize_terminal_text(text <> cont), info}} | tail]
[{idx, %{output | text: text <> cont}} | tail]
end
defp add_output(outputs, {idx, {type, text, info}}) when type in [:plain_text, :markdown] do
[{idx, {type, normalize_terminal_text(text), info}} | outputs]
end
defp add_output(outputs, {idx, {type, container_outputs, info}}) when type in [:frame, :grid] do
[{idx, {type, merge_chunk_outputs(container_outputs), info}} | outputs]
defp add_output(outputs, {idx, output}) when output.type in [:frame, :grid] do
output = update_in(output.outputs, &merge_chunk_outputs/1)
[{idx, output} | outputs]
end
defp add_output(outputs, output), do: [output | outputs]
@ -810,9 +794,9 @@ defmodule Livebook.Notebook do
Enum.map_reduce(outputs, counter, &index_output/2)
end
defp index_output({type, outputs, info}, counter) when type in [:frame, :tabs, :grid] do
{outputs, counter} = index_outputs(outputs, counter)
{{counter, {type, outputs, info}}, counter + 1}
defp index_output(output, counter) when output.type in [:frame, :tabs, :grid] do
{outputs, counter} = index_outputs(output.outputs, counter)
{{counter, %{output | outputs: outputs}}, counter + 1}
end
defp index_output(output, counter) do
@ -831,18 +815,15 @@ defmodule Livebook.Notebook do
do: frame_output
end
defp do_find_frame_outputs({_idx, {:frame, _outputs, %{ref: ref}}} = output, ref) do
defp do_find_frame_outputs({_idx, %{type: :frame, ref: ref}} = output, ref) do
[output]
end
defp do_find_frame_outputs({_idx, {type, outputs, _info}}, ref)
when type in [:frame, :tabs, :grid] do
Enum.flat_map(outputs, &do_find_frame_outputs(&1, ref))
defp do_find_frame_outputs({_idx, output}, ref) when output.type in [:frame, :tabs, :grid] do
Enum.flat_map(output.outputs, &do_find_frame_outputs(&1, ref))
end
defp do_find_frame_outputs(_output, _ref) do
[]
end
defp do_find_frame_outputs(_output, _ref), do: []
@doc """
Removes outputs that get rendered only once.
@ -864,45 +845,43 @@ defmodule Livebook.Notebook do
defp do_prune_outputs([], _appendable?, acc), do: acc
# Keep trailing outputs that can be merged with subsequent outputs
defp do_prune_outputs([{idx, {type, _, %{chunk: true} = info}}], true = _appendable?, acc)
defp do_prune_outputs([{idx, %{type: type, chunk: true} = output}], true = _appendable?, acc)
when type in [:terminal_text, :plain_text, :markdown] do
[{idx, {type, :__pruned__, info}} | acc]
[{idx, %{output | text: :__pruned__}} | acc]
end
# Keep frame and its relevant contents
defp do_prune_outputs([{idx, {:frame, frame_outputs, info}} | outputs], appendable?, acc) do
do_prune_outputs(
outputs,
appendable?,
[{idx, {:frame, prune_outputs(frame_outputs, true), info}} | acc]
)
defp do_prune_outputs([{idx, %{type: :frame} = output} | outputs], appendable?, acc) do
output = update_in(output.outputs, &prune_outputs(&1, true))
do_prune_outputs(outputs, appendable?, [{idx, output} | acc])
end
# Keep layout output and its relevant contents
defp do_prune_outputs([{idx, {:tabs, tabs_outputs, info}} | outputs], appendable?, acc) do
case prune_outputs(tabs_outputs, false) do
defp do_prune_outputs([{idx, %{type: :tabs} = output} | outputs], appendable?, acc) do
case prune_outputs(output.outputs, false) do
[] ->
do_prune_outputs(outputs, appendable?, acc)
pruned_tabs_outputs ->
info = Map.replace(info, :labels, :__pruned__)
do_prune_outputs(outputs, appendable?, [{idx, {:tabs, pruned_tabs_outputs, info}} | acc])
output = %{output | outputs: pruned_tabs_outputs, labels: :__pruned__}
do_prune_outputs(outputs, appendable?, [{idx, output} | acc])
end
end
defp do_prune_outputs([{idx, {:grid, grid_outputs, info}} | outputs], appendable?, acc) do
case prune_outputs(grid_outputs, false) do
defp do_prune_outputs([{idx, %{type: :grid} = output} | outputs], appendable?, acc) do
case prune_outputs(output.outputs, false) do
[] ->
do_prune_outputs(outputs, appendable?, acc)
pruned_grid_outputs ->
do_prune_outputs(outputs, appendable?, [{idx, {:grid, pruned_grid_outputs, info}} | acc])
output = %{output | outputs: pruned_grid_outputs}
do_prune_outputs(outputs, appendable?, [{idx, output} | acc])
end
end
# Keep outputs that get re-rendered
defp do_prune_outputs([{idx, output} | outputs], appendable?, acc)
when elem(output, 0) in [:input, :control, :error] do
when output.type in [:input, :control, :error] do
do_prune_outputs(outputs, appendable?, [{idx, output} | acc])
end

View file

@ -51,19 +51,19 @@ defmodule Livebook.Notebook.Cell do
@doc """
Extracts all inputs from the given indexed output.
"""
@spec find_inputs_in_output(indexed_output()) :: list(input_attrs :: map())
@spec find_inputs_in_output(indexed_output()) :: list(Livebook.Runtime.input_output())
def find_inputs_in_output(output)
def find_inputs_in_output({_idx, {:input, attrs}}) do
[attrs]
def find_inputs_in_output({_idx, %{type: :input} = input}) do
[input]
end
def find_inputs_in_output({_idx, {:control, %{type: :form, fields: fields}}}) do
def find_inputs_in_output({_idx, %{type: :control, attrs: %{type: :form, fields: fields}}}) do
Keyword.values(fields)
end
def find_inputs_in_output({_idx, {type, outputs, _}}) when type in [:frame, :tabs, :grid] do
Enum.flat_map(outputs, &find_inputs_in_output/1)
def find_inputs_in_output({_idx, output}) when output.type in [:frame, :tabs, :grid] do
Enum.flat_map(output.outputs, &find_inputs_in_output/1)
end
def find_inputs_in_output(_output), do: []
@ -74,13 +74,13 @@ defmodule Livebook.Notebook.Cell do
@spec find_assets_in_output(Livebook.Runtime.output()) :: list(asset_info :: map())
def find_assets_in_output(output)
def find_assets_in_output({:js, %{js_view: %{assets: assets_info}}}), do: [assets_info]
def find_assets_in_output(%{type: :js} = output), do: [output.js_view.assets]
def find_assets_in_output({type, outputs, _}) when type in [:frame, :tabs, :grid] do
Enum.flat_map(outputs, &find_assets_in_output/1)
def find_assets_in_output(output) when output.type in [:frame, :tabs, :grid] do
Enum.flat_map(output.outputs, &find_assets_in_output/1)
end
def find_assets_in_output(_), do: []
def find_assets_in_output(_output), do: []
@setup_cell_id "setup"

View file

@ -67,37 +67,391 @@ defprotocol Livebook.Runtime do
@typedoc """
An output emitted during evaluation or as the final result.
For more details on output types see `t:Kino.Output.t/0`.
"""
@type output ::
:ignored
# Text with terminal style and ANSI support
| {:terminal_text, text :: String.t(), info :: map()}
# Plain text content
| {:plain_text, text :: String.t(), info :: map()}
# Markdown content
| {:markdown, text :: String.t(), info :: map()}
# A raw image in the given format
| {:image, content :: binary(), mime_type :: binary()}
# JavaScript powered output
| {:js, info :: map()}
# Outputs placeholder
| {:frame, outputs :: list(output()), info :: map()}
# Outputs in tabs
| {:tabs, outputs :: list(output()), info :: map()}
# Outputs in grid
| {:grid, outputs :: list(output()), info :: map()}
# An input field
| {:input, attrs :: map()}
# A control element
| {:control, attrs :: map()}
# Internal output format for errors
| {:error, message :: String.t(),
type ::
{:missing_secret, name :: String.t()}
| {:interrupt, variant :: :normal | :error, message :: String.t()}
| :other}
ignored_output()
| terminal_text_output()
| plain_text_output()
| markdown_output()
| image_output()
| js_output()
| frame_output()
| frame_update_output()
| tabs_output()
| grid_output()
| input_output()
| control_output()
| error_output()
@typedoc """
An empty output that should be ignored whenever encountered.
"""
@type ignored_output :: %{type: :ignored}
@typedoc ~S"""
Terminal text content.
Supports ANSI escape codes and overwriting lines with `\r`.
Adjacent outputs with `:chunk` set to `true` are merged and rendered
as a whole.
"""
@type terminal_text_output :: %{type: :terminal_text, text: String.t(), chunk: boolean()}
@typedoc """
Plain text content.
Adjacent outputs with `:chunk` set to `true` are merged and rendered
as a whole.
Similar to `t:markdown/0`, but with no special markup.
"""
@type plain_text_output :: %{type: :plain_text, text: String.t(), chunk: boolean()}
@typedoc """
Markdown content.
Adjacent outputs with `:chunk` set to `true` are merged and rendered
as a whole.
"""
@type markdown_output :: %{type: :markdown, text: String.t(), chunk: boolean()}
@typedoc """
A raw image in the given format.
## Pixel data
Note that a special `image/x-pixel` MIME type is supported. The
binary consists of the following consecutive parts:
* height - 32 bits (unsigned big-endian integer)
* width - 32 bits (unsigned big-endian integer)
* channels - 8 bits (unsigned integer)
* data - pixel data in HWC order
Pixel data consists of 8-bit unsigned integers. The number of channels
can be either: 1 (grayscale), 2 (grayscale + alpha), 3 (RGB), or 4
(RGB + alpha).
"""
@type image_output :: %{type: :image, content: binary(), mime_type: String.t()}
@typedoc """
JavaScript powered output with dynamic data and events.
See `Kino.JS` and `Kino.JS.Live` for more details.
## Export
The `:export` map describes how the output should be persisted.
The output data is put in a Markdown fenced code block.
* `:info_string` - used as the info string for the Markdown
code block
* `:key` - in case the data is a map and only a specific part
should be exported
"""
@type js_output() :: %{
type: :js,
js_view: js_view(),
export:
nil
| %{
info_string: String.t(),
key: nil | term()
}
}
@typedoc """
A JavaScript view definition.
JS view is a component rendered on the client side and possibly
interacting with a server process within the runtime.
* `:ref` - unique identifier
* `:pid` - the server process holding the data and handling
interactions
## Assets
The `:assets` map includes information about the relevant files.
* `:archive_path` - an absolute path to a `.tar.gz` archive with
all the assets
* `:hash` - a checksum of all assets in the archive
* `:js_path` - a relative asset path pointing to the JavaScript
entrypoint module
* `:cdn_url` - an absolute URL to a CDN directory where the asset
files can be accessed from. Note that this URL is not guaranteed
to be accessible, since it may be pointing to a private package
## Communication protocol
A client process should connect to the server process by sending:
{:connect, pid(), info :: %{ref: ref(), origin: term()}}
And expect the following reply:
{:connect_reply, payload, info :: %{ref: ref()}}
The server process may then keep sending one of the following events:
{:event, event :: String.t(), payload, info :: %{ref: ref()}}
The client process may keep sending one of the following events:
{:event, event :: String.t(), payload, info :: %{ref: ref(), origin: term()}}
The client can also send a ping message:
{:ping, pid(), metadata :: term(), info :: %{ref: ref()}}
And the server should respond with:
{:pong, metadata :: term(), info :: %{ref: ref()}}
"""
@type js_view :: %{
ref: ref(),
pid: Process.dest(),
assets: %{
archive_path: String.t(),
hash: String.t(),
js_path: String.t(),
cdn_url: String.t() | nil
}
}
@typedoc """
An area to dynamically put outputs into.
This output includes the initial outputs and can be later updated
by sending `t:frame_update_output/0` outputs.
The outputs order is always reversed, that is, most recent outputs
are at the top of the stack.
"""
@type frame_output :: %{
type: :frame,
ref: frame_ref(),
outputs: list(output()),
placeholder: boolean()
}
@typedoc """
An output emitted to update and existing `t:frame_output/0`.
"""
@type frame_update_output :: %{
type: :frame_update,
ref: frame_ref(),
update: {:replace, list(output())} | {:append, list(output())}
}
@type frame_ref :: String.t()
@typedoc """
Multiple outputs arranged into tabs.
"""
@type tabs_output :: %{type: :tabs, outputs: list(t()), labels: list(String.t())}
@typedoc """
Multiple outputs arranged in a grid.
"""
@type grid_output :: %{
type: :grid,
outputs: list(t()),
columns: pos_integer(),
gap: non_neg_integer(),
boxed: boolean()
}
@typedoc """
An input field.
* `:ref` - a unique identifier
* `:id` - a persistent input identifier, the same on every
reevaluation
* `:default` - the initial input value
* `:destination` - the process to send event messages to
* `:attrs` - input-specific attributes. The required fields are
`:type` and `:default`
"""
@type input_output :: %{
type: :input,
ref: ref(),
id: input_id(),
destination: Process.dest(),
attrs: input_attrs()
}
@type input_id :: String.t()
@type input_attrs ::
%{
type: :text,
default: String.t(),
label: String.t()
}
| %{
type: :textarea,
default: String.t(),
label: String.t(),
monospace: boolean()
}
| %{
type: :password,
default: String.t(),
label: String.t()
}
| %{
type: :number,
default: number() | nil,
label: String.t()
}
| %{
type: :url,
default: String.t() | nil,
label: String.t()
}
| %{
type: :select,
default: term(),
label: String.t(),
options: list({value :: term(), label :: String.t()})
}
| %{
type: :checkbox,
default: boolean(),
label: String.t()
}
| %{
type: :range,
default: number(),
label: String.t(),
min: number(),
max: number(),
step: number()
}
| %{
type: :utc_datetime,
default: NaiveDateTime.t() | nil,
label: String.t(),
min: NaiveDateTime.t() | nil,
max: NaiveDateTime.t() | nil
}
| %{
type: :utc_time,
default: Time.t() | nil,
label: String.t(),
min: Time.t() | nil,
max: Time.t() | nil
}
| %{
type: :date,
default: Date.t(),
label: String.t(),
min: Date.t(),
max: Date.t()
}
| %{
type: :color,
default: String.t(),
label: String.t()
}
| %{
type: :image,
default: nil,
label: String.t(),
format: :rgb | :png | :jpeg,
size: {pos_integer(), pos_integer()} | nil,
fit: :match | :contain | :pad | :crop
}
| %{
type: :audio,
default: nil,
label: String.t(),
format: :pcm_f32 | :wav,
sampling_rate: pos_integer()
}
| %{
type: :file,
default: nil,
label: String.t(),
accept: list(String.t()) | :any
}
@typedoc """
A control widget.
* `:ref` - a unique identifier
* `:destination` - the process to send event messages to
* `:attrs` - control-specific attributes. The only required field
is `:type`
## Events
All control events are sent to `:destination` as `{:event, id, info}`,
where info is a map including additional details. In particular, it
always includes `:origin`, which is an opaque identifier of the client
that triggered the event.
"""
@type control_output :: %{
type: :control,
ref: ref(),
destination: Process.dest(),
attrs: control_attrs()
}
@type control_attrs ::
%{
type: :keyboard,
events: list(:keyup | :keydown | :status),
default_handlers: :off | :on | :disable_only
}
| %{
type: :button,
label: String.t()
}
| %{
type: :form,
fields: list({field :: atom(), input_output()}),
submit: String.t() | nil,
# Currently we always use true, but we can support
# other tracking modes in the future
report_changes: %{(field :: atom()) => true},
reset_on_submit: list(field :: atom())
}
@type ref :: String.t()
@typedoc """
Error content, usually representing evaluation failure.
The `:known_reason` field is used by Livebook to suggest a way to fix
the error.
"""
@type error_output :: %{
type: :error,
message: String.t(),
known_reason:
{:missing_secret, name :: String.t()}
| {:interrupt, variant :: :normal | :error, message :: String.t()}
| {:file_entry_forbidden, name :: String.t()}
| :other
}
@typedoc """
Additional information about a completed evaluation.
@ -330,22 +684,6 @@ defprotocol Livebook.Runtime do
packages: list(package())
}
@typedoc """
A JavaScript view definition.
See `t:Kino.Output.js_view/0` for details.
"""
@type js_view :: %{
ref: String.t(),
pid: Process.dest(),
assets: %{
archive_path: String.t(),
hash: String.t(),
js_path: String.t(),
cdn_url: String.t() | nil
}
}
@type smart_cell_ref :: String.t()
@type smart_cell_attrs :: map()

View file

@ -20,7 +20,7 @@ defmodule Livebook.Runtime.Evaluator.Formatter do
# Functions in the `IEx.Helpers` module return this specific value
# to indicate no result should be printed in the iex shell,
# so we respect that as well.
:ignored
%{type: :ignored}
end
def format_result({:ok, {:module, _, _, _} = value}, :elixir) do
@ -40,13 +40,13 @@ defmodule Livebook.Runtime.Evaluator.Formatter do
|> error_color
|> :erlang.list_to_binary()
{:error, formatted, error_type(error)}
%{type: :error, message: formatted, known_reason: error_known_reason(error)}
end
end
def format_result({:error, kind, error, stacktrace}, _language) do
formatted = format_error(kind, error, stacktrace)
{:error, formatted, error_type(error)}
%{type: :error, message: formatted, known_reason: error_known_reason(error)}
end
def format_result({:ok, value}, :erlang) do
@ -76,11 +76,11 @@ defmodule Livebook.Runtime.Evaluator.Formatter do
defp to_inspect_output(value, opts \\ []) do
try do
inspected = inspect(value, inspect_opts(opts))
{:terminal_text, inspected, %{chunk: false}}
%{type: :terminal_text, text: inspected, chunk: false}
catch
kind, error ->
formatted = format_error(kind, error, __STACKTRACE__)
{:error, formatted, error_type(error)}
%{type: :error, message: formatted, known_reason: error_known_reason(error)}
end
end
@ -159,19 +159,19 @@ defmodule Livebook.Runtime.Evaluator.Formatter do
IO.ANSI.format([:red, string], true)
end
defp error_type(%System.EnvError{env: "LB_" <> secret_name}),
defp error_known_reason(%System.EnvError{env: "LB_" <> secret_name}),
do: {:missing_secret, secret_name}
defp error_type(error) when is_struct(error, Kino.InterruptError),
defp error_known_reason(error) when is_struct(error, Kino.InterruptError),
do: {:interrupt, error.variant, error.message}
defp error_type(error) when is_struct(error, Kino.FS.ForbiddenError),
defp error_known_reason(error) when is_struct(error, Kino.FS.ForbiddenError),
do: {:file_entry_forbidden, error.name}
defp error_type(_), do: :other
defp error_known_reason(_), do: :other
defp erlang_to_output(value) do
text = :io_lib.format("~p", [value]) |> IO.iodata_to_binary()
{:terminal_text, text, %{chunk: false}}
%{type: :terminal_text, text: text, chunk: false}
end
end

View file

@ -438,10 +438,8 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do
string = state.buffer |> Enum.reverse() |> Enum.join()
if state.send_to != nil and string != "" do
send(
state.send_to,
{:runtime_evaluation_output, state.ref, {:terminal_text, string, %{chunk: true}}}
)
output = %{type: :terminal_text, text: string, chunk: true}
send(state.send_to, {:runtime_evaluation_output, state.ref, output})
end
%{state | buffer: []}

View file

@ -1448,11 +1448,14 @@ defmodule Livebook.Session do
end
def handle_info({:runtime_evaluation_output, cell_id, output}, state) do
output = normalize_runtime_output(output)
operation = {:add_cell_evaluation_output, @client_id, cell_id, output}
{:noreply, handle_operation(state, operation)}
end
def handle_info({:runtime_evaluation_output_to, client_id, cell_id, output}, state) do
output = normalize_runtime_output(output)
client_pid =
Enum.find_value(state.client_pids_with_id, fn {pid, id} ->
id == client_id && pid
@ -1483,6 +1486,7 @@ defmodule Livebook.Session do
end
def handle_info({:runtime_evaluation_output_to_clients, cell_id, output}, state) do
output = normalize_runtime_output(output)
operation = {:add_cell_evaluation_output, @client_id, cell_id, output}
broadcast_operation(state.session_id, operation)
@ -1503,9 +1507,10 @@ defmodule Livebook.Session do
{:noreply, state}
end
def handle_info({:runtime_evaluation_response, cell_id, response, metadata}, state) do
def handle_info({:runtime_evaluation_response, cell_id, output, metadata}, state) do
{memory_usage, metadata} = Map.pop(metadata, :memory_usage)
operation = {:add_cell_evaluation_response, @client_id, cell_id, response, metadata}
output = normalize_runtime_output(output)
operation = {:add_cell_evaluation_response, @client_id, cell_id, output, metadata}
{:noreply,
state
@ -2863,4 +2868,112 @@ defmodule Livebook.Session do
{:error, "download failed, " <> message, status}
end
end
# Maps legacy outputs and adds missing attributes
defp normalize_runtime_output(output) when is_map(output), do: output
defp normalize_runtime_output(:ignored) do
%{type: :ignored}
end
defp normalize_runtime_output({:text, text}) do
%{type: :terminal_text, text: text, chunk: false}
end
defp normalize_runtime_output({:plain_text, text}) do
%{type: :plain_text, text: text, chunk: false}
end
defp normalize_runtime_output({:markdown, text}) do
%{type: :markdown, text: text, chunk: false}
end
defp normalize_runtime_output({:image, content, mime_type}) do
%{type: :image, content: content, mime_type: mime_type}
end
defp normalize_runtime_output({:js, info}) do
%{type: :js, js_view: info.js_view, export: info.export}
end
defp normalize_runtime_output({:frame, outputs, %{ref: ref, type: :default} = info}) do
%{
type: :frame,
ref: ref,
outputs: Enum.map(outputs, &normalize_runtime_output/1),
placeholder: Map.get(info, :placeholder, true)
}
end
defp normalize_runtime_output({:frame, outputs, %{ref: ref, type: :replace}}) do
%{
type: :frame_update,
ref: ref,
update: {:replace, Enum.map(outputs, &normalize_runtime_output/1)}
}
end
defp normalize_runtime_output({:frame, outputs, %{ref: ref, type: :append}}) do
%{
type: :frame_update,
ref: ref,
update: {:append, Enum.map(outputs, &normalize_runtime_output/1)}
}
end
defp normalize_runtime_output({:tabs, outputs, %{labels: labels}}) do
%{type: :tabs, outputs: Enum.map(outputs, &normalize_runtime_output/1), labels: labels}
end
defp normalize_runtime_output({:grid, outputs, info}) do
%{
type: :grid,
outputs: Enum.map(outputs, &normalize_runtime_output/1),
columns: Map.get(info, :columns, 1),
gap: Map.get(info, :gap, 8),
boxed: Map.get(info, :boxed, false)
}
end
defp normalize_runtime_output({:input, attrs}) do
{fields, attrs} = Map.split(attrs, [:ref, :id, :destination])
attrs =
case attrs.type do
:textarea -> Map.put_new(attrs, :monospace, false)
_other -> attrs
end
Map.merge(fields, %{type: :input, attrs: attrs})
end
defp normalize_runtime_output({:control, attrs}) do
{fields, attrs} = Map.split(attrs, [:ref, :destination])
attrs =
case attrs.type do
:keyboard ->
Map.put_new(attrs, :default_handlers, :off)
:form ->
Map.update!(attrs, :fields, fn fields ->
Enum.map(fields, fn {field, attrs} ->
{field, normalize_runtime_output({:input, attrs})}
end)
end)
_other ->
attrs
end
Map.merge(fields, %{type: :control, attrs: attrs})
end
defp normalize_runtime_output({:error, message, type}) do
%{type: :error, message: message, known_reason: type}
end
defp normalize_runtime_output(other) do
%{type: :unknown, output: other}
end
end

View file

@ -333,9 +333,9 @@ defmodule Livebook.Session.Data do
cell <- section.cells,
Cell.evaluable?(cell),
output <- cell.outputs,
attrs <- Cell.find_inputs_in_output(output),
input <- Cell.find_inputs_in_output(output),
into: %{},
do: {attrs.id, input_info(attrs.default)}
do: {input.id, input_info(input.attrs.default)}
end
@doc """
@ -1245,7 +1245,7 @@ defmodule Livebook.Session.Data do
new_input_infos =
indexed_output
|> Cell.find_inputs_in_output()
|> Map.new(fn attrs -> {attrs.id, input_info(attrs.default)} end)
|> Map.new(fn input -> {input.id, input_info(input.attrs.default)} end)
{data, _} =
data_actions =

View file

@ -384,14 +384,14 @@ defmodule LivebookWeb.AppSessionLive do
defp update_data_view(data_view, _prev_data, data, operation) do
case operation do
# See LivebookWeb.SessionLive for more details
{:add_cell_evaluation_output, _client_id, _cell_id,
{:frame, _outputs, %{type: type, ref: ref}}}
when type != :default ->
for {idx, {:frame, frame_outputs, _}} <- Notebook.find_frame_outputs(data.notebook, ref) do
{:add_cell_evaluation_output, _client_id, _cell_id, %{type: :frame_update} = output} ->
%{ref: ref, update: {update_type, _}} = output
for {idx, frame} <- Notebook.find_frame_outputs(data.notebook, ref) do
send_update(LivebookWeb.Output.FrameComponent,
id: "output-#{idx}",
outputs: frame_outputs,
update_type: type
outputs: frame.outputs,
update_type: update_type
)
end
@ -438,7 +438,7 @@ defmodule LivebookWeb.AppSessionLive do
end
defp input_views_for_output(output, data, changed_input_ids) do
input_ids = for attrs <- Cell.find_inputs_in_output(output), do: attrs.id
input_ids = for input <- Cell.find_inputs_in_output(output), do: input.id
data.input_infos
|> Map.take(input_ids)
@ -463,34 +463,34 @@ defmodule LivebookWeb.AppSessionLive do
end
defp filter_output({idx, output})
when elem(output, 0) in [:plain_text, :markdown, :image, :js, :control, :input],
when output.type in [:plain_text, :markdown, :image, :js, :control, :input],
do: {idx, output}
defp filter_output({idx, {:tabs, outputs, metadata}}) do
defp filter_output({idx, %{type: :tabs} = output}) do
outputs_with_labels =
for {output, label} <- Enum.zip(outputs, metadata.labels),
for {output, label} <- Enum.zip(output.outputs, output.labels),
output = filter_output(output),
do: {output, label}
{outputs, labels} = Enum.unzip(outputs_with_labels)
{idx, {:tabs, outputs, %{metadata | labels: labels}}}
{idx, %{output | outputs: outputs, labels: labels}}
end
defp filter_output({idx, {:grid, outputs, metadata}}) do
outputs = rich_outputs(outputs)
defp filter_output({idx, %{type: :grid} = output}) do
outputs = rich_outputs(output.outputs)
if outputs != [] do
{idx, {:grid, outputs, metadata}}
{idx, %{output | outputs: outputs}}
end
end
defp filter_output({idx, {:frame, outputs, metadata}}) do
outputs = rich_outputs(outputs)
{idx, {:frame, outputs, metadata}}
defp filter_output({idx, %{type: :frame} = output}) do
outputs = rich_outputs(output.outputs)
{idx, %{output | outputs: outputs}}
end
defp filter_output({idx, {:error, _message, {:interrupt, _, _}} = output}),
defp filter_output({idx, %{type: :error, known_reason: {:interrupt, _, _}} = output}),
do: {idx, output}
defp filter_output(_output), do: nil

View file

@ -37,47 +37,50 @@ defmodule LivebookWeb.Output do
"""
end
defp border?({:terminal_text, _text, _info}), do: true
defp border?({:plain_text, _text, _info}), do: true
defp border?({:error, _message, {:interrupt, _, _}}), do: false
defp border?({:error, _message, _type}), do: true
defp border?({:grid, _, info}), do: Map.get(info, :boxed, false)
defp border?(%{type: type}) when type in [:terminal_text, :plain_text], do: true
defp border?(%{type: :error, known_reason: {:interrupt, _, _}}), do: false
defp border?(%{type: :error}), do: true
defp border?(%{type: :grid, boxed: boxed}), do: boxed
defp border?(_output), do: false
defp render_output({:terminal_text, text, _info}, %{id: id}) do
defp render_output(%{type: :terminal_text, text: text}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
live_component(Output.TerminalTextComponent, id: id, text: text)
end
defp render_output({:plain_text, text, _info}, %{id: id}) do
defp render_output(%{type: :plain_text, text: text}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
live_component(Output.PlainTextComponent, id: id, text: text)
end
defp render_output({:markdown, text, _info}, %{id: id, session_id: session_id}) do
defp render_output(%{type: :markdown, text: text}, %{id: id, session_id: session_id}) do
text = if(text == :__pruned__, do: nil, else: text)
live_component(Output.MarkdownComponent, id: id, session_id: session_id, text: text)
end
defp render_output({:image, content, mime_type}, %{id: id}) do
assigns = %{id: id, content: content, mime_type: mime_type}
defp render_output(%{type: :image} = output, %{id: id}) do
assigns = %{id: id, content: output.content, mime_type: output.mime_type}
~H"""
<Output.ImageComponent.render id={@id} content={@content} mime_type={@mime_type} />
"""
end
defp render_output({:js, js_info}, %{id: id, session_id: session_id, client_id: client_id}) do
defp render_output(%{type: :js} = output, %{
id: id,
session_id: session_id,
client_id: client_id
}) do
live_component(LivebookWeb.JSViewComponent,
id: id,
js_view: js_info.js_view,
js_view: output.js_view,
session_id: session_id,
client_id: client_id,
timeout_message: "Output data no longer available, please reevaluate this cell"
)
end
defp render_output({:frame, outputs, info}, %{
defp render_output(%{type: :frame} = output, %{
id: id,
session_id: session_id,
session_pid: session_pid,
@ -87,8 +90,8 @@ defmodule LivebookWeb.Output do
}) do
live_component(Output.FrameComponent,
id: id,
outputs: outputs,
placeholder: Map.get(info, :placeholder, true),
outputs: output.outputs,
placeholder: output.placeholder,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
@ -97,7 +100,7 @@ defmodule LivebookWeb.Output do
)
end
defp render_output({:tabs, outputs, info}, %{
defp render_output(%{type: :tabs, outputs: outputs, labels: labels}, %{
id: id,
session_id: session_id,
session_pid: session_pid,
@ -106,11 +109,11 @@ defmodule LivebookWeb.Output do
cell_id: cell_id
}) do
{labels, active_idx} =
if info.labels == :__pruned__ do
if labels == :__pruned__ do
{[], nil}
else
labels =
Enum.zip_with(info.labels, outputs, fn label, {output_idx, _} -> {output_idx, label} end)
Enum.zip_with(labels, outputs, fn label, {output_idx, _} -> {output_idx, label} end)
active_idx = get_in(outputs, [Access.at(0), Access.elem(0)])
@ -173,7 +176,7 @@ defmodule LivebookWeb.Output do
"""
end
defp render_output({:grid, outputs, info}, %{
defp render_output(%{type: :grid} = grid, %{
id: id,
session_id: session_id,
session_pid: session_pid,
@ -181,14 +184,11 @@ defmodule LivebookWeb.Output do
client_id: client_id,
cell_id: cell_id
}) do
columns = info[:columns] || 1
gap = info[:gap] || 8
assigns = %{
id: id,
columns: columns,
gap: gap,
outputs: outputs,
columns: grid.columns,
gap: grid.gap,
outputs: grid.outputs,
session_id: session_id,
session_pid: session_pid,
input_views: input_views,
@ -220,7 +220,7 @@ defmodule LivebookWeb.Output do
"""
end
defp render_output({:input, attrs}, %{
defp render_output(%{type: :input} = input, %{
id: id,
input_views: input_views,
session_pid: session_pid,
@ -228,14 +228,14 @@ defmodule LivebookWeb.Output do
}) do
live_component(Output.InputComponent,
id: id,
attrs: attrs,
input: input,
input_views: input_views,
session_pid: session_pid,
client_id: client_id
)
end
defp render_output({:control, attrs}, %{
defp render_output(%{type: :control} = control, %{
id: id,
input_views: input_views,
session_pid: session_pid,
@ -244,7 +244,7 @@ defmodule LivebookWeb.Output do
}) do
live_component(Output.ControlComponent,
id: id,
attrs: attrs,
control: control,
input_views: input_views,
session_pid: session_pid,
client_id: client_id,
@ -252,14 +252,11 @@ defmodule LivebookWeb.Output do
)
end
defp render_output({:error, formatted, {:missing_secret, secret_name}}, %{
session_id: session_id
}) do
assigns = %{
message: formatted,
secret_name: secret_name,
session_id: session_id
}
defp render_output(
%{type: :error, known_reason: {:missing_secret, secret_name}} = output,
%{session_id: session_id}
) do
assigns = %{message: output.message, secret_name: secret_name, session_id: session_id}
~H"""
<div class="-m-4 space-x-4 py-4">
@ -283,14 +280,11 @@ defmodule LivebookWeb.Output do
"""
end
defp render_output({:error, formatted, {:file_entry_forbidden, file_entry_name}}, %{
session_id: session_id
}) do
assigns = %{
message: formatted,
file_entry_name: file_entry_name,
session_id: session_id
}
defp render_output(
%{type: :error, known_reason: {:file_entry_forbidden, file_entry_name}} = output,
%{session_id: session_id}
) do
assigns = %{message: output.message, file_entry_name: file_entry_name, session_id: session_id}
~H"""
<div class="-m-4 space-x-4 py-4">
@ -314,7 +308,10 @@ defmodule LivebookWeb.Output do
"""
end
defp render_output({:error, _formatted, {:interrupt, variant, message}}, %{cell_id: cell_id}) do
defp render_output(
%{type: :error, known_reason: {:interrupt, variant, message}},
%{cell_id: cell_id}
) do
assigns = %{variant: variant, message: message, cell_id: cell_id}
~H"""
@ -346,8 +343,8 @@ defmodule LivebookWeb.Output do
"""
end
defp render_output({:error, formatted, _type}, %{}) do
render_formatted_error_message(formatted)
defp render_output(%{type: :error, message: message}, %{}) do
render_formatted_error_message(message)
end
defp render_output(output, %{}) do

View file

@ -7,16 +7,16 @@ defmodule LivebookWeb.Output.ControlComponent do
end
@impl true
def render(%{attrs: %{type: :keyboard}} = assigns) do
def render(assigns) when assigns.control.attrs.type == :keyboard do
~H"""
<div
class="flex"
id={"#{@id}-root"}
phx-hook="KeyboardControl"
data-cell-id={@cell_id}
data-default-handlers={Map.get(@attrs, :default_handlers, :off)}
data-keydown-enabled={to_string(@keyboard_enabled and :keydown in @attrs.events)}
data-keyup-enabled={to_string(@keyboard_enabled and :keyup in @attrs.events)}
data-default-handlers={@control.default_handlers.attrs}
data-keydown-enabled={to_string(@keyboard_enabled and :keydown in @control.attrs.events)}
data-keyup-enabled={to_string(@keyboard_enabled and :keyup in @control.attrs.events)}
data-target={@myself}
>
<span class="tooltip right" data-tooltip="Toggle keyboard control">
@ -35,7 +35,7 @@ defmodule LivebookWeb.Output.ControlComponent do
"""
end
def render(%{attrs: %{type: :button}} = assigns) do
def render(assigns) when assigns.control.attrs.type == :button do
~H"""
<div class="flex">
<button
@ -43,19 +43,19 @@ defmodule LivebookWeb.Output.ControlComponent do
type="button"
phx-click={JS.push("button_click", target: @myself)}
>
<%= @attrs.label %>
<%= @control.attrs.label %>
</button>
</div>
"""
end
def render(%{attrs: %{type: :form}} = assigns) do
def render(assigns) when assigns.control.attrs.type == :form do
~H"""
<div>
<.live_component
module={LivebookWeb.Output.ControlFormComponent}
id={@id}
attrs={@attrs}
control={@control}
input_views={@input_views}
session_pid={@session_pid}
client_id={@client_id}
@ -67,7 +67,7 @@ defmodule LivebookWeb.Output.ControlComponent do
def render(assigns) do
~H"""
<div class="text-red-600">
Unknown control type <%= @attrs.type %>
Unknown control type <%= @control.attrs.type %>
</div>
"""
end
@ -113,8 +113,8 @@ defmodule LivebookWeb.Output.ControlComponent do
end
defp report_event(socket, attrs) do
topic = socket.assigns.attrs.ref
topic = socket.assigns.control.ref
event = Map.merge(%{origin: socket.assigns.client_id}, attrs)
send(socket.assigns.attrs.destination, {:event, topic, event})
send(socket.assigns.control.destination, {:event, topic, event})
end
end

View file

@ -13,14 +13,14 @@ defmodule LivebookWeb.Output.ControlFormComponent do
socket = assign(socket, assigns)
data =
Map.new(assigns.attrs.fields, fn {field, input_attrs} ->
{field, assigns.input_views[input_attrs.id].value}
Map.new(assigns.control.attrs.fields, fn {field, input} ->
{field, assigns.input_views[input.id].value}
end)
if data != prev_data do
change_data =
for {field, value} <- data,
assigns.attrs.report_changes[field],
assigns.control.attrs.report_changes[field],
into: %{},
do: {field, value}
@ -33,22 +33,22 @@ defmodule LivebookWeb.Output.ControlFormComponent do
end
@impl true
def render(%{attrs: %{type: :form}} = assigns) do
def render(assigns) do
~H"""
<div class="flex flex-col space-y-3">
<.live_component
:for={{_field, input_attrs} <- @attrs.fields}
:for={{_field, input} <- @control.attrs.fields}
module={LivebookWeb.Output.InputComponent}
id={"#{@id}-#{input_attrs.id}"}
attrs={input_attrs}
id={"#{@id}-#{input.id}"}
input={input}
input_views={@input_views}
session_pid={@session_pid}
client_id={@client_id}
local={true}
/>
<div :if={@attrs.submit}>
<div :if={@control.attrs.submit}>
<button class="button-base button-blue" type="button" phx-click="submit" phx-target={@myself}>
<%= @attrs.submit %>
<%= @control.attrs.submit %>
</button>
</div>
</div>
@ -59,7 +59,7 @@ defmodule LivebookWeb.Output.ControlFormComponent do
def handle_event("submit", %{}, socket) do
report_event(socket, %{type: :submit, data: socket.assigns.data})
if socket.assigns.attrs.reset_on_submit do
if socket.assigns.control.attrs.reset_on_submit do
reset_inputs(socket)
end
@ -67,16 +67,16 @@ defmodule LivebookWeb.Output.ControlFormComponent do
end
defp report_event(socket, attrs) do
topic = socket.assigns.attrs.ref
topic = socket.assigns.control.ref
event = Map.merge(%{origin: socket.assigns.client_id}, attrs)
send(socket.assigns.attrs.destination, {:event, topic, event})
send(socket.assigns.control.destination, {:event, topic, event})
end
defp reset_inputs(socket) do
values =
for {field, input_attrs} <- socket.assigns.attrs.fields,
field in socket.assigns.attrs.reset_on_submit,
do: {input_attrs.id, input_attrs.default}
for {field, input} <- socket.assigns.control.attrs.fields,
field in socket.assigns.control.attrs.reset_on_submit,
do: {input.id, input.attrs.default}
send(self(), {:set_input_values, values, true})
end

View file

@ -12,7 +12,7 @@ defmodule LivebookWeb.Output.InputComponent do
end
def update(assigns, socket) do
%{value: value, changed: changed} = assigns.input_views[assigns.attrs.id]
%{value: value, changed: changed} = assigns.input_views[assigns.input.id]
socket =
socket
@ -23,51 +23,51 @@ defmodule LivebookWeb.Output.InputComponent do
end
@impl true
def render(%{attrs: %{type: :image}} = assigns) do
def render(assigns) when assigns.input.attrs.type == :image do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label label={@attrs.label} changed={@changed} />
<.input_label label={@input.attrs.label} changed={@changed} />
<.live_component
module={LivebookWeb.Output.ImageInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
height={@attrs.size && elem(@attrs.size, 0)}
width={@attrs.size && elem(@attrs.size, 1)}
format={@attrs.format}
fit={@attrs.fit}
height={@input.attrs.size && elem(@input.attrs.size, 0)}
width={@input.attrs.size && elem(@input.attrs.size, 1)}
format={@input.attrs.format}
fit={@input.attrs.fit}
/>
</div>
"""
end
def render(%{attrs: %{type: :audio}} = assigns) do
def render(assigns) when assigns.input.attrs.type == :audio do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label label={@attrs.label} changed={@changed} />
<.input_label label={@input.attrs.label} changed={@changed} />
<.live_component
module={LivebookWeb.Output.AudioInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
format={@attrs.format}
sampling_rate={@attrs.sampling_rate}
format={@input.attrs.format}
sampling_rate={@input.attrs.sampling_rate}
/>
</div>
"""
end
def render(%{attrs: %{type: :file}} = assigns) do
def render(assigns) when assigns.input.attrs.type == :file do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label label={@attrs.label} changed={@changed} />
<.input_label label={@input.attrs.label} changed={@changed} />
<.live_component
module={LivebookWeb.Output.FileInputComponent}
id={"#{@id}-input"}
input_component_id={@id}
value={@value}
accept={@attrs.accept}
input_id={@attrs.id}
accept={@input.attrs.accept}
input_id={@input.id}
session_pid={@session_pid}
client_id={@client_id}
local={@local}
@ -76,11 +76,11 @@ defmodule LivebookWeb.Output.InputComponent do
"""
end
def render(%{attrs: %{type: :utc_datetime}} = assigns) do
def render(assigns) when assigns.input.attrs.type == :utc_datetime do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label
label={@attrs.label}
label={@input.attrs.label}
changed={@changed}
help="Choose the time in your local time zone"
/>
@ -94,19 +94,19 @@ defmodule LivebookWeb.Output.InputComponent do
autocomplete="off"
phx-hook="UtcDateTimeInput"
data-utc-value={@value && NaiveDateTime.to_iso8601(@value)}
data-utc-min={@attrs.min && NaiveDateTime.to_iso8601(@attrs.min)}
data-utc-max={@attrs.max && NaiveDateTime.to_iso8601(@attrs.max)}
data-utc-min={@input.attrs.min && NaiveDateTime.to_iso8601(@input.attrs.min)}
data-utc-max={@input.attrs.max && NaiveDateTime.to_iso8601(@input.attrs.max)}
data-phx-target={@myself}
/>
</div>
"""
end
def render(%{attrs: %{type: :utc_time}} = assigns) do
def render(assigns) when assigns.input.attrs.type == :utc_time do
~H"""
<div id={"#{@id}-form-#{@counter}"}>
<.input_label
label={@attrs.label}
label={@input.attrs.label}
changed={@changed}
help="Choose the time in your local time zone"
/>
@ -120,8 +120,8 @@ defmodule LivebookWeb.Output.InputComponent do
autocomplete="off"
phx-hook="UtcTimeInput"
data-utc-value={@value && Time.to_iso8601(@value)}
data-utc-min={@attrs.min && Time.to_iso8601(@attrs.min)}
data-utc-max={@attrs.max && Time.to_iso8601(@attrs.max)}
data-utc-min={@input.attrs.min && Time.to_iso8601(@input.attrs.min)}
data-utc-max={@input.attrs.max && Time.to_iso8601(@input.attrs.max)}
data-phx-target={@myself}
/>
</div>
@ -131,8 +131,8 @@ defmodule LivebookWeb.Output.InputComponent do
def render(assigns) do
~H"""
<form id={"#{@id}-form-#{@counter}"} phx-change="change" phx-submit="submit" phx-target={@myself}>
<.input_label label={@attrs.label} changed={@changed} />
<.input_output id={"#{@id}-input"} attrs={@attrs} value={@value} myself={@myself} />
<.input_label label={@input.attrs.label} changed={@changed} />
<.input_output id={"#{@id}-input"} attrs={@input.attrs} value={@value} myself={@myself} />
</form>
"""
end
@ -187,7 +187,7 @@ defmodule LivebookWeb.Output.InputComponent do
<textarea
id={@id}
data-el-input
class={["input min-h-[38px] max-h-[300px] tiny-scrollbar", @attrs[:monospace] && "font-mono"]}
class={["input min-h-[38px] max-h-[300px] tiny-scrollbar", @attrs.monospace && "font-mono"]}
name="html_value"
phx-hook="TextareaAutosize"
phx-debounce="blur"
@ -252,7 +252,7 @@ defmodule LivebookWeb.Output.InputComponent do
defp input_output(assigns) do
~H"""
<div class="text-red-600">
Unknown input type <%= @attrs.type %>
Unknown input type <%= @input.attrs.type %>
</div>
"""
end
@ -281,7 +281,7 @@ defmodule LivebookWeb.Output.InputComponent do
@impl true
def handle_event("change", %{"html_value" => html_value}, socket) do
case parse(html_value, socket.assigns.attrs) do
case parse(html_value, socket.assigns.input.attrs) do
{:ok, value} ->
{:noreply, handle_change(socket, value)}
@ -292,10 +292,10 @@ defmodule LivebookWeb.Output.InputComponent do
end
def handle_event("submit", %{"html_value" => html_value}, socket) do
case parse(html_value, socket.assigns.attrs) do
case parse(html_value, socket.assigns.input.attrs) do
{:ok, value} ->
socket = handle_change(socket, value)
send(self(), {:queue_bound_cells_evaluation, socket.assigns.attrs.id})
send(self(), {:queue_bound_cells_evaluation, socket.assigns.input.id})
{:noreply, socket}
:error ->
@ -316,7 +316,7 @@ defmodule LivebookWeb.Output.InputComponent do
end
defp report_change(%{assigns: assigns} = socket) do
send(self(), {:set_input_values, [{assigns.attrs.id, assigns.value}], assigns.local})
send(self(), {:set_input_values, [{assigns.input.id, assigns.value}], assigns.local})
unless assigns.local do
report_event(socket, assigns.value)
@ -443,8 +443,8 @@ defmodule LivebookWeb.Output.InputComponent do
end
defp report_event(socket, value) do
topic = socket.assigns.attrs.ref
topic = socket.assigns.input.ref
event = %{value: value, origin: socket.assigns.client_id, type: :change}
send(socket.assigns.attrs.destination, {:event, topic, event})
send(socket.assigns.input.destination, {:event, topic, event})
end
end

View file

@ -2740,8 +2740,8 @@ defmodule LivebookWeb.SessionLive do
defp input_views_for_cell(cell, data, changed_input_ids) do
input_ids =
for output <- cell.outputs,
attrs <- Cell.find_inputs_in_output(output),
do: attrs.id
input <- Cell.find_inputs_in_output(output),
do: input.id
data.input_infos
|> Map.take(input_ids)
@ -2770,24 +2770,24 @@ defmodule LivebookWeb.SessionLive do
# For outputs that update existing outputs we send the update directly
# to the corresponding component, so the DOM patch is isolated and fast.
# This is important for intensive output updates
{:add_cell_evaluation_output, _client_id, _cell_id,
{:frame, _outputs, %{type: type, ref: ref}}}
when type != :default ->
for {idx, {:frame, frame_outputs, _}} <- Notebook.find_frame_outputs(data.notebook, ref) do
{:add_cell_evaluation_output, _client_id, _cell_id, %{type: :frame_update} = output} ->
%{ref: ref, update: {update_type, _}} = output
for {idx, frame} <- Notebook.find_frame_outputs(data.notebook, ref) do
send_update(LivebookWeb.Output.FrameComponent,
id: "output-#{idx}",
outputs: frame_outputs,
update_type: type
outputs: frame.outputs,
update_type: update_type
)
end
data_view
{:add_cell_evaluation_output, _client_id, cell_id, {type, text, %{chunk: true}}}
{:add_cell_evaluation_output, _client_id, cell_id, %{type: type, chunk: true} = output}
when type in [:terminal_text, :plain_text, :markdown] ->
# Lookup in previous data to see if the output is already there
case Notebook.fetch_cell_and_section(prev_data.notebook, cell_id) do
{:ok, %{outputs: [{idx, {^type, _, %{chunk: true}}} | _]}, _section} ->
{:ok, %{outputs: [{idx, %{type: ^type, chunk: true}} | _]}, _section} ->
module =
case type do
:terminal_text -> LivebookWeb.Output.TerminalTextComponent
@ -2795,7 +2795,7 @@ defmodule LivebookWeb.SessionLive do
:markdown -> LivebookWeb.Output.MarkdownComponent
end
send_update(module, id: "output-#{idx}", text: text)
send_update(module, id: "output-#{idx}", text: output.text)
data_view
_ ->

View file

@ -577,7 +577,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
IO.puts("hey")\
""",
outputs: [
{0, {:terminal_text, "hey", %{chunk: true}}}
{0, terminal_text("hey", true)}
]
}
]
@ -614,7 +614,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
outputs: [{0, terminal_text("hey", true)}]
}
]
}
@ -657,8 +657,8 @@ defmodule Livebook.LiveMarkdown.ExportTest do
IO.puts("hey")\
""",
outputs: [
{0, {:terminal_text, "\e[34m:ok\e[0m", %{chunk: false}}},
{1, {:terminal_text, "hey", %{chunk: true}}}
{0, terminal_text("\e[34m:ok\e[0m")},
{1, terminal_text("hey", true)}
]
}
]
@ -707,7 +707,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:markdown, "some **Markdown**"}}]
outputs: [{0, %{type: :markdown, text: "some **Markdown**", chunk: false}}]
}
]
}
@ -788,15 +788,15 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: ":ok",
outputs: [
{0,
{:js,
%{
js_view: %{
ref: "1",
pid: spawn_widget_with_data("1", "graph TD;\nA-->B;"),
assets: %{archive_path: "", hash: "abcd", js_path: "main.js"}
},
export: %{info_string: "mermaid", key: nil}
}}}
%{
type: :js,
js_view: %{
ref: "1",
pid: spawn_widget_with_data("1", "graph TD;\nA-->B;"),
assets: %{archive_path: "", hash: "abcd", js_path: "main.js"}
},
export: %{info_string: "mermaid", key: nil}
}}
]
}
]
@ -840,15 +840,15 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: ":ok",
outputs: [
{0,
{:js,
%{
js_view: %{
ref: "1",
pid: spawn_widget_with_data("1", %{height: 50, width: 50}),
assets: %{archive_path: "", hash: "abcd", js_path: "main.js"}
},
export: %{info_string: "box", key: nil}
}}}
%{
type: :js,
js_view: %{
ref: "1",
pid: spawn_widget_with_data("1", %{height: 50, width: 50}),
assets: %{archive_path: "", hash: "abcd", js_path: "main.js"}
},
export: %{info_string: "box", key: nil}
}}
]
}
]
@ -891,19 +891,19 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: ":ok",
outputs: [
{0,
{:js,
%{
js_view: %{
ref: "1",
pid:
spawn_widget_with_data("1", %{
spec: %{"height" => 50, "width" => 50},
datasets: []
}),
assets: %{archive_path: "", hash: "abcd", js_path: "main.js"}
},
export: %{info_string: "vega-lite", key: :spec}
}}}
%{
type: :js,
js_view: %{
ref: "1",
pid:
spawn_widget_with_data("1", %{
spec: %{"height" => 50, "width" => 50},
datasets: []
}),
assets: %{archive_path: "", hash: "abcd", js_path: "main.js"}
},
export: %{info_string: "vega-lite", key: :spec}
}}
]
}
]
@ -947,12 +947,15 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: ":ok",
outputs: [
{0,
{:tabs,
[
{1, {:markdown, "a"}},
{2, {:terminal_text, "b", %{chunk: false}}},
{3, {:terminal_text, "c", %{chunk: false}}}
], %{labels: ["A", "B", "C"]}}}
%{
type: :tabs,
outputs: [
{1, %{type: :markdown, text: "a", chunk: false}},
{2, terminal_text("b")},
{3, terminal_text("c")}
],
labels: ["A", "B", "C"]
}}
]
}
]
@ -995,12 +998,17 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: ":ok",
outputs: [
{0,
{:grid,
[
{1, {:terminal_text, "a", %{chunk: false}}},
{2, {:markdown, "b"}},
{3, {:terminal_text, "c", %{chunk: false}}}
], %{columns: 2}}}
%{
type: :grid,
outputs: [
{1, terminal_text("a")},
{2, %{type: :markdown, text: "b", chunk: false}},
{3, terminal_text("c")}
],
columns: 2,
gap: 8,
boxed: false
}}
]
}
]
@ -1050,7 +1058,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
outputs: [{0, terminal_text("hey", true)}]
}
]
}
@ -1095,7 +1103,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
outputs: [{0, terminal_text("hey", true)}]
}
]
}

View file

@ -643,8 +643,8 @@ defmodule Livebook.LiveMarkdown.ImportTest do
IO.puts("hey")\
""",
outputs: [
{0, {:terminal_text, ":ok", %{chunk: false}}},
{1, {:terminal_text, "hey", %{chunk: false}}}
{0, terminal_text(":ok")},
{1, terminal_text("hey")}
]
}
]
@ -886,8 +886,8 @@ defmodule Livebook.LiveMarkdown.ImportTest do
IO.puts("hey")\
""",
outputs: [
{0, {:terminal_text, ":ok", %{chunk: false}}},
{1, {:terminal_text, "hey", %{chunk: false}}}
{0, terminal_text(":ok")},
{1, terminal_text("hey")}
]
}
]

View file

@ -1,6 +1,8 @@
defmodule Livebook.NotebookTest do
use ExUnit.Case, async: true
import Livebook.TestHelpers
alias Livebook.Notebook
alias Livebook.Notebook.{Section, Cell}
@ -264,8 +266,7 @@ defmodule Livebook.NotebookTest do
describe "find_asset_info/2" do
test "returns asset info matching the given type if found" do
assets_info = %{archive: "/path/to/archive.tar.gz", hash: "abcd", js_path: "main.js"}
js_info = %{js_view: %{assets: assets_info}}
output = {:js, js_info}
output = %{type: :js, js_view: %{assets: assets_info}, export: nil}
notebook = %{
Notebook.new()
@ -293,7 +294,7 @@ defmodule Livebook.NotebookTest do
%{
Cell.new(:code)
| id: "c1",
outputs: [{0, {:terminal_text, "Hola", %{chunk: true}}}]
outputs: [{0, terminal_text("Hola", true)}]
}
]
}
@ -304,14 +305,14 @@ defmodule Livebook.NotebookTest do
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:terminal_text, "Hola amigo!", %{chunk: true}}}]}]
cells: [%{outputs: [{0, terminal_text("Hola amigo!", true)}]}]
}
]
} =
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, " amigo!", %{chunk: true}}
terminal_text(" amigo!", true)
)
end
@ -326,7 +327,7 @@ defmodule Livebook.NotebookTest do
%{
Cell.new(:code)
| id: "c1",
outputs: [{0, {:terminal_text, "Hola", %{chunk: false}}}]
outputs: [{0, terminal_text("Hola")}]
}
]
}
@ -340,8 +341,8 @@ defmodule Livebook.NotebookTest do
cells: [
%{
outputs: [
{1, {:terminal_text, " amigo!", %{chunk: true}}},
{0, {:terminal_text, "Hola", %{chunk: false}}}
{1, terminal_text(" amigo!", true)},
{0, terminal_text("Hola")}
]
}
]
@ -351,7 +352,7 @@ defmodule Livebook.NotebookTest do
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, " amigo!", %{chunk: true}}
terminal_text(" amigo!", true)
)
end
@ -373,14 +374,14 @@ defmodule Livebook.NotebookTest do
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:terminal_text, "Hey", %{chunk: false}}}]}]
cells: [%{outputs: [{0, terminal_text("Hey")}]}]
}
]
} =
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, "Hola\rHey", %{chunk: false}}
terminal_text("Hola\rHey")
)
end
@ -395,7 +396,7 @@ defmodule Livebook.NotebookTest do
%{
Cell.new(:code)
| id: "c1",
outputs: [{0, {:terminal_text, "Hola", %{chunk: true}}}]
outputs: [{0, terminal_text("Hola", true)}]
}
]
}
@ -406,14 +407,14 @@ defmodule Livebook.NotebookTest do
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:terminal_text, "amigo!\r", %{chunk: true}}}]}]
cells: [%{outputs: [{0, terminal_text("amigo!\r", true)}]}]
}
]
} =
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, "\ramigo!\r", %{chunk: true}}
terminal_text("\ramigo!\r", true)
)
end
@ -428,12 +429,12 @@ defmodule Livebook.NotebookTest do
%{
Cell.new(:code)
| id: "c1",
outputs: [{0, {:frame, [], %{ref: "1", type: :default}}}]
outputs: [{0, %{type: :frame, ref: "1", outputs: [], placeholder: true}}]
},
%{
Cell.new(:code)
| id: "c2",
outputs: [{1, {:frame, [], %{ref: "1", type: :default}}}]
outputs: [{1, %{type: :frame, ref: "1", outputs: [], placeholder: true}}]
}
]
}
@ -447,16 +448,12 @@ defmodule Livebook.NotebookTest do
cells: [
%{
outputs: [
{0,
{:frame, [{2, {:terminal_text, "hola", %{chunk: false}}}],
%{ref: "1", type: :default}}}
{0, %{type: :frame, ref: "1", outputs: [{2, terminal_text("hola")}]}}
]
},
%{
outputs: [
{1,
{:frame, [{3, {:terminal_text, "hola", %{chunk: false}}}],
%{ref: "1", type: :default}}}
{1, %{type: :frame, ref: "1", outputs: [{3, terminal_text("hola")}]}}
]
}
]
@ -466,8 +463,7 @@ defmodule Livebook.NotebookTest do
Notebook.add_cell_output(
notebook,
"c2",
{:frame, [{:terminal_text, "hola", %{chunk: false}}],
%{ref: "1", type: :replace}}
%{type: :frame_update, ref: "1", update: {:replace, [terminal_text("hola")]}}
)
end
@ -491,23 +487,23 @@ defmodule Livebook.NotebookTest do
%{
outputs: [
{2,
{:frame, [{1, {:terminal_text, "hola amigo!", %{chunk: true}}}],
%{ref: "1", type: :default}}}
%{
type: :frame,
ref: "1",
outputs: [{1, terminal_text("hola amigo!", true)}]
}}
]
}
]
}
]
} =
Notebook.add_cell_output(
notebook,
"c1",
{:frame,
[
{:terminal_text, " amigo!", %{chunk: true}},
{:terminal_text, "hola", %{chunk: true}}
], %{ref: "1", type: :default}}
)
Notebook.add_cell_output(notebook, "c1", %{
type: :frame,
ref: "1",
outputs: [terminal_text(" amigo!", true), terminal_text("hola", true)],
placeholder: true
})
end
test "skips ignored output" do
@ -529,43 +525,13 @@ defmodule Livebook.NotebookTest do
cells: [%{outputs: []}]
}
]
} = Notebook.add_cell_output(notebook, "c1", :ignored)
end
test "supports legacy text outputs" do
notebook = %{
Notebook.new()
| sections: [
%{
Section.new()
| id: "s1",
cells: [%{Cell.new(:code) | id: "c1", outputs: []}]
}
],
output_counter: 0
}
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:terminal_text, "Hola", %{chunk: false}}}]}]
}
]
} = Notebook.add_cell_output(notebook, "c1", {:text, "Hola"})
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:markdown, "Hola", %{chunk: false}}}]}]
}
]
} = Notebook.add_cell_output(notebook, "c1", {:markdown, "Hola"})
} = Notebook.add_cell_output(notebook, "c1", %{type: :ignored})
end
end
describe "find_frame_outputs/2" do
test "returns frame outputs with matching ref" do
frame_output = {0, {:frame, [], %{ref: "1", type: :default}}}
frame_output = {0, %{type: :frame, ref: "1", outputs: [], placeholder: true}}
notebook = %{
Notebook.new()
@ -576,8 +542,8 @@ defmodule Livebook.NotebookTest do
end
test "finds a nested frame" do
nested_frame_output = {0, {:frame, [], %{ref: "2", type: :default}}}
frame_output = {0, {:frame, [nested_frame_output], %{ref: "1", type: :default}}}
nested_frame_output = {0, %{type: :frame, ref: "2", outputs: [], placeholder: true}}
frame_output = {0, %{type: :frame, ref: "1", outputs: [nested_frame_output]}}
notebook = %{
Notebook.new()

View file

@ -9,6 +9,12 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
{:ok, %{pid: runtime_server_pid}}
end
defmacrop terminal_text(text, chunk \\ false) do
quote do
%{type: :terminal_text, text: unquote(text), chunk: unquote(chunk)}
end
end
describe "attach/2" do
test "starts watching the given process and terminates as soon as it terminates" do
owner =
@ -63,7 +69,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
[]
)
assert_receive {:runtime_evaluation_output, :e1, {:terminal_text, output, %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :e1, terminal_text(output, true)}
assert output =~ "error to stdout\n"
end
@ -77,8 +83,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, [])
assert_receive {:runtime_evaluation_output, :e1,
{:terminal_text, log_message, %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :e1, terminal_text(log_message, true)}
assert log_message =~ "[error] hey"
end
@ -89,7 +94,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
RuntimeServer.evaluate_code(pid, :elixir, "x", {:c2, :e2}, [{:c1, :e1}])
assert_receive {:runtime_evaluation_response, :e2, {:terminal_text, "\e[34m1\e[0m", %{}},
assert_receive {:runtime_evaluation_response, :e2, terminal_text("\e[34m1\e[0m"),
%{evaluation_time_ms: _time_ms}}
end

View file

@ -1,6 +1,8 @@
defmodule Livebook.Runtime.Evaluator.IOProxyTest do
use ExUnit.Case, async: true
import Livebook.TestHelpers
alias Livebook.Runtime.Evaluator
alias Livebook.Runtime.Evaluator.IOProxy
@ -18,17 +20,17 @@ defmodule Livebook.Runtime.Evaluator.IOProxyTest do
describe ":stdio interoperability" do
test "IO.puts", %{io: io} do
IO.puts(io, "hey")
assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "hey\n", %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :ref, terminal_text("hey\n", true)}
end
test "IO.write", %{io: io} do
IO.write(io, "hey")
assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "hey", %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :ref, terminal_text("hey", true)}
end
test "IO.inspect", %{io: io} do
IO.inspect(io, %{}, [])
assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "%{}\n", %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :ref, terminal_text("%{}\n", true)}
end
test "IO.read", %{io: io} do
@ -84,36 +86,32 @@ defmodule Livebook.Runtime.Evaluator.IOProxyTest do
IO.puts(io, "hey")
IO.puts(io, "hey")
assert_receive {:runtime_evaluation_output, :ref,
{:terminal_text, "hey\nhey\n", %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :ref, terminal_text("hey\nhey\n", true)}
end
test "respects CR as line cleaner", %{io: io} do
IO.write(io, "hey")
IO.write(io, "\roverride\r")
assert_receive {:runtime_evaluation_output, :ref,
{:terminal_text, "\roverride\r", %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :ref, terminal_text("\roverride\r", true)}
end
test "after_evaluation/1 synchronously sends buffer contents", %{io: io} do
IO.puts(io, "hey")
IOProxy.after_evaluation(io)
assert_received {:runtime_evaluation_output, :ref, {:terminal_text, "hey\n", %{chunk: true}}}
assert_received {:runtime_evaluation_output, :ref, terminal_text("hey\n", true)}
end
test "supports direct livebook output forwarding", %{io: io} do
livebook_put_output(io, {:terminal_text, "[1, 2, 3]", %{chunk: false}})
livebook_put_output(io, terminal_text("[1, 2, 3]"))
assert_received {:runtime_evaluation_output, :ref,
{:terminal_text, "[1, 2, 3]", %{chunk: false}}}
assert_received {:runtime_evaluation_output, :ref, terminal_text("[1, 2, 3]")}
end
test "supports direct livebook output forwarding for a specific client", %{io: io} do
livebook_put_output_to(io, "client1", {:terminal_text, "[1, 2, 3]", %{chunk: false}})
livebook_put_output_to(io, "client1", terminal_text("[1, 2, 3]"))
assert_received {:runtime_evaluation_output_to, "client1", :ref,
{:terminal_text, "[1, 2, 3]", %{chunk: false}}}
assert_received {:runtime_evaluation_output_to, "client1", :ref, terminal_text("[1, 2, 3]")}
end
describe "token requests" do

View file

@ -39,6 +39,18 @@ defmodule Livebook.Runtime.EvaluatorTest do
defmacrop ansi_number(number), do: "\e[34m#{number}\e[0m"
defmacrop ansi_string(string), do: "\e[32m\"#{string}\"\e[0m"
defmacrop terminal_text(text, chunk \\ false) do
quote do
%{type: :terminal_text, text: unquote(text), chunk: unquote(chunk)}
end
end
defmacrop error(message) do
quote do
%{type: :error, message: unquote(message), known_reason: :other}
end
end
describe "evaluate_code/6" do
test "given a valid code returns evaluation result", %{evaluator: evaluator} do
code = """
@ -49,8 +61,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, ansi_number(3), %{}}, metadata() = metadata}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(ansi_number(3)),
metadata() = metadata}
assert metadata.evaluation_time_ms >= 0
@ -65,9 +77,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2,
{:error,
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
:other}, metadata()}
error(
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m"
), metadata()}
end
test "given parent refs sees previous evaluation context", %{evaluator: evaluator} do
@ -76,15 +88,15 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [:code_1])
assert_receive {:runtime_evaluation_response, :code_2,
{:terminal_text, ansi_number(1), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2, terminal_text(ansi_number(1)),
metadata()}
end
test "given invalid parent ref uses the default context", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, :elixir, "1", :code_1, [:code_nonexistent])
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, ansi_number(1), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(ansi_number(1)),
metadata()}
end
test "given parent refs sees previous process dictionary", %{evaluator: evaluator} do
@ -95,13 +107,13 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, "Process.get(:x)", :code_3, [:code_1])
assert_receive {:runtime_evaluation_response, :code_3,
{:terminal_text, ansi_number(1), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_3, terminal_text(ansi_number(1)),
metadata()}
Evaluator.evaluate_code(evaluator, :elixir, "Process.get(:x)", :code_3, [:code_2])
assert_receive {:runtime_evaluation_response, :code_3,
{:terminal_text, ansi_number(2), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_3, terminal_text(ansi_number(2)),
metadata()}
end
test "keeps :rand state intact in process dictionary", %{evaluator: evaluator} do
@ -110,13 +122,11 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, result1, %{}},
metadata()}
assert_receive {:runtime_evaluation_response, :code_2, terminal_text(result1), metadata()}
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, result2, %{}},
metadata()}
assert_receive {:runtime_evaluation_response, :code_2, terminal_text(result2), metadata()}
assert result1 != result2
@ -125,20 +135,17 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, ^result1, %{}},
metadata()}
assert_receive {:runtime_evaluation_response, :code_2, terminal_text(^result1), metadata()}
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, ^result2, %{}},
metadata()}
assert_receive {:runtime_evaluation_response, :code_2, terminal_text(^result2), metadata()}
end
test "captures standard output and sends it to the caller", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, :elixir, ~s{IO.puts("hey")}, :code_1, [])
assert_receive {:runtime_evaluation_output, :code_1,
{:terminal_text, "hey\n", %{chunk: true}}}
assert_receive {:runtime_evaluation_output, :code_1, terminal_text("hey\n", true)}
end
test "using livebook input sends input request to the caller", %{evaluator: evaluator} do
@ -156,8 +163,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert_receive {:runtime_evaluation_input_request, :code_1, reply_to, "input1"}
send(reply_to, {:runtime_evaluation_input_reply, {:ok, 10}})
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, ansi_number(10), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(ansi_number(10)),
metadata()}
end
test "returns error along with its kind and stacktrace", %{evaluator: evaluator} do
@ -167,8 +174,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
metadata()}
assert_receive {:runtime_evaluation_response, :code_1, error(message), metadata()}
assert """
** (FunctionClauseError) no function clause matching in List.first/2
@ -194,7 +200,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
assert_receive {:runtime_evaluation_response, :code_1, error(message),
%{
code_markers: [
%{
@ -219,9 +225,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_1,
{:error,
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
:other},
error(
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m"
),
%{
code_markers: [
%{
@ -245,9 +251,10 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_1,
{:error,
"\e[31m** (CompileError) file.ex: cannot compile module Livebook.Runtime.EvaluatorTest.Invalid " <>
"(errors have been logged)\e[0m" <> _, :other},
error(
"\e[31m** (CompileError) file.ex: cannot compile module Livebook.Runtime.EvaluatorTest.Invalid " <>
"(errors have been logged)\e[0m" <> _
),
%{
code_markers: [
%{
@ -267,9 +274,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_1,
{:error,
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
:other}, %{code_markers: []}}
error(
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m"
), %{code_markers: []}}
end
test "in case of an error returns only the relevant part of stacktrace",
@ -295,8 +302,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
# Note: evaluating module definitions is relatively slow, so we use a higher wait timeout.
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
metadata()},
assert_receive {:runtime_evaluation_response, :code_1, error(message), metadata()},
2_000
assert clean_message(message) ==
@ -324,17 +330,17 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code1, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, ansi_number(2), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(ansi_number(2)),
metadata()}
Evaluator.evaluate_code(evaluator, :elixir, code2, :code_2, [:code_1])
assert_receive {:runtime_evaluation_response, :code_2, {:error, _, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2, error(_), metadata()}
Evaluator.evaluate_code(evaluator, :elixir, code3, :code_3, [:code_2, :code_1])
assert_receive {:runtime_evaluation_response, :code_3,
{:terminal_text, ansi_number(4), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_3, terminal_text(ansi_number(4)),
metadata()}
end
test "given file option sets it in evaluation environment", %{evaluator: evaluator} do
@ -346,7 +352,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], opts)
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, ansi_string("/path/dir"), %{}}, metadata()}
terminal_text(ansi_string("/path/dir")), metadata()}
end
test "kills widgets that that no evaluation points to", %{evaluator: evaluator} do
@ -356,8 +362,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, widget_pid1_string, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(widget_pid1_string),
metadata()}
widget_pid1 = IEx.Helpers.pid(widget_pid1_string)
@ -365,8 +371,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, widget_pid2_string, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(widget_pid2_string),
metadata()}
widget_pid2 = IEx.Helpers.pid(widget_pid2_string)
@ -387,8 +393,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
[]
)
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, widget_pid1_string, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(widget_pid1_string),
metadata()}
widget_pid1 = IEx.Helpers.pid(widget_pid1_string)
@ -404,18 +410,18 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(_), metadata()}
# Redefining in the same evaluation works
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(_), metadata()}
Evaluator.evaluate_code(evaluator, :elixir, code, :code_2, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_2,
{:error,
"\e[31m** (CompileError) file.ex:1: module Livebook.Runtime.EvaluatorTest.Redefinition is already defined\e[0m",
:other},
error(
"\e[31m** (CompileError) file.ex:1: module Livebook.Runtime.EvaluatorTest.Redefinition is already defined\e[0m"
),
%{
code_markers: [
%{
@ -438,7 +444,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(_), metadata()}
assert File.exists?(Path.join(ebin_path, "Elixir.Livebook.Runtime.EvaluatorTest.Disk.beam"))
@ -454,7 +460,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:error, _, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, error(_), metadata()}
refute Code.ensure_loaded?(Livebook.Runtime.EvaluatorTest.Raised)
end
@ -742,7 +748,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
ref = eval_idx
parent_refs = Enum.to_list((eval_idx - 1)..0//-1)
Evaluator.evaluate_code(evaluator, :elixir, code, ref, parent_refs)
assert_receive {:runtime_evaluation_response, ^ref, {:terminal_text, _, %{}}, metadata}
assert_receive {:runtime_evaluation_response, ^ref, terminal_text(_), metadata}
%{used: metadata.identifiers_used, defined: metadata.identifiers_defined}
end
@ -1150,16 +1156,16 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [:code_1])
assert_receive {:runtime_evaluation_response, :code_2,
{:error,
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
:other}, metadata()}
error(
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m"
), metadata()}
end
test "kills widgets that no evaluation points to", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, widget_pid1_string, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(widget_pid1_string),
metadata()}
widget_pid1 = IEx.Helpers.pid(widget_pid1_string)
@ -1177,13 +1183,13 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(_), metadata()}
Evaluator.forget_evaluation(evaluator, :code_1)
# Define the module in a different evaluation
Evaluator.evaluate_code(evaluator, :elixir, code, :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, _, %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2, terminal_text(_), metadata()}
end
end
@ -1206,8 +1212,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2,
{:terminal_text, ansi_number(1), %{}}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2, terminal_text(ansi_number(1)),
metadata()}
end
end
@ -1246,8 +1252,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
[]
)
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, "6", %{}},
metadata()}
assert_receive {:runtime_evaluation_response, :code_1, terminal_text("6"), metadata()}
end
test "mixed erlang/elixir bindings", %{evaluator: evaluator} do
@ -1265,15 +1270,14 @@ defmodule Livebook.Runtime.EvaluatorTest do
code = ~S"#{x=>1}."
Evaluator.evaluate_code(evaluator, :erlang, code, :code_1, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, ~S"#{x => 1}", %{}},
assert_receive {:runtime_evaluation_response, :code_1, terminal_text(~S"#{x => 1}"),
metadata()}
end
test "does not return error marker on empty source", %{evaluator: evaluator} do
Evaluator.evaluate_code(evaluator, :erlang, "", :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:error, _, _},
metadata() = metadata}
assert_receive {:runtime_evaluation_response, :code_1, error(_), metadata() = metadata}
assert metadata.code_markers == []
end
@ -1281,22 +1285,22 @@ defmodule Livebook.Runtime.EvaluatorTest do
test "syntax and tokenizer errors are converted", %{evaluator: evaluator} do
# Incomplete input
Evaluator.evaluate_code(evaluator, :erlang, "X =", :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, error(message), metadata()}
assert "\e[31m** (TokenMissingError)" <> _ = message
# Parser error
Evaluator.evaluate_code(evaluator, :erlang, "X ==/== a.", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:error, message, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2, error(message), metadata()}
assert "\e[31m** (SyntaxError)" <> _ = message
# Tokenizer error
Evaluator.evaluate_code(evaluator, :erlang, "$a$", :code_3, [])
assert_receive {:runtime_evaluation_response, :code_3, {:error, message, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_3, error(message), metadata()}
assert "\e[31m** (SyntaxError)" <> _ = message
# Erlang exception
Evaluator.evaluate_code(evaluator, :erlang, "list_to_binary(1).", :code_4, [])
assert_receive {:runtime_evaluation_response, :code_4, {:error, message, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_4, error(message), metadata()}
assert "\e[31mexception error: bad argument" <> _ = message
end
end
@ -1306,8 +1310,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
code = "%Livebook.TestModules.BadInspect{}"
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
metadata()}
assert_receive {:runtime_evaluation_response, :code_1, error(message), metadata()}
assert message =~ ":bad_return"
end

View file

@ -7,11 +7,20 @@ defmodule Livebook.Session.DataTest do
alias Livebook.{Delta, Notebook}
alias Livebook.Users.User
@eval_resp {:ok, [1, 2, 3]}
@eval_resp %{type: :terminal_text, text: ":ok", chunk: false}
@smart_cell_definitions [%{kind: "text", name: "Text", requirement_presets: []}]
@stdout {:terminal_text, "Hello!", %{chunk: true}}
@cid "__anonymous__"
@stdout %{type: :terminal_text, text: "Hello!", chunk: true}
@input %{
type: :input,
ref: "ref1",
id: "i1",
destination: nil,
attrs: %{type: :text, default: "hey", label: "Text"}
}
defp eval_meta(opts \\ []) do
uses = opts[:uses] || []
defines = opts[:defines] || %{}
@ -863,8 +872,6 @@ defmodule Livebook.Session.DataTest do
end
test "garbage collects input values that are no longer used" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -872,7 +879,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
{:set_input_value, @cid, "i1", "value"}
])
@ -886,8 +893,6 @@ defmodule Livebook.Session.DataTest do
end
test "marks cells bound to the deleted input as stale" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -896,7 +901,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
evaluate_cells_operations(["c2"], %{bind_inputs: %{"c2" => ["i1"]}})
])
@ -1875,8 +1880,6 @@ defmodule Livebook.Session.DataTest do
end
test "stores default values for new inputs in the pre-evaluation data" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -1886,7 +1889,7 @@ defmodule Livebook.Session.DataTest do
{:queue_cells_evaluation, @cid, ["c1"]}
])
operation = {:add_cell_evaluation_output, @cid, "c1", {:input, input}}
operation = {:add_cell_evaluation_output, @cid, "c1", @input}
assert {:ok,
%{
@ -1910,14 +1913,14 @@ defmodule Livebook.Session.DataTest do
{:queue_cells_evaluation, @cid, ["c1"]}
])
operation = {:add_cell_evaluation_response, @cid, "c1", {:ok, [1, 2, 3]}, eval_meta()}
operation = {:add_cell_evaluation_response, @cid, "c1", @eval_resp, eval_meta()}
assert {:ok,
%{
notebook: %{
sections: [
%{
cells: [%{outputs: [{1, {:ok, [1, 2, 3]}}]}]
cells: [%{outputs: [{1, @eval_resp}]}]
}
]
}
@ -2297,8 +2300,6 @@ defmodule Livebook.Session.DataTest do
end
test "if bound input value changes during cell evaluation, the cell is marked as stale afterwards" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -2307,7 +2308,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1", "c2"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
{:add_cell_evaluation_response, @cid, "c2", @eval_resp, eval_meta()},
# Make the code cell evaluating
{:queue_cells_evaluation, @cid, ["c2"]},
@ -2365,8 +2366,6 @@ defmodule Livebook.Session.DataTest do
end
test "stores default values for new inputs" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -2376,15 +2375,14 @@ defmodule Livebook.Session.DataTest do
{:queue_cells_evaluation, @cid, ["c1"]}
])
operation = {:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()}
operation = {:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()}
assert {:ok, %{input_infos: %{"i1" => %{value: "hey"}}}, _} =
Data.apply_operation(data, operation)
end
test "stores default values for new nested inputs" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
output = {:grid, [{:input, input}], %{}}
output = %{type: :grid, outputs: [@input], columns: 1, gap: 8, boxed: false}
data =
data_after_operations!([
@ -2402,8 +2400,6 @@ defmodule Livebook.Session.DataTest do
end
test "keeps input values for inputs that existed" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -2411,21 +2407,19 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
{:set_input_value, @cid, "i1", "value"},
{:queue_cells_evaluation, @cid, ["c1"]}
])
# Output the same input again
operation = {:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()}
operation = {:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()}
assert {:ok, %{input_infos: %{"i1" => %{value: "value"}}}, _} =
Data.apply_operation(data, operation)
end
test "garbage collects input values that are no longer used" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -2433,7 +2427,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
{:set_input_value, @cid, "i1", "value"},
{:queue_cells_evaluation, @cid, ["c1"]}
])
@ -2449,8 +2443,6 @@ defmodule Livebook.Session.DataTest do
end
test "does not garbage collect inputs if present in another cell" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -2459,8 +2451,8 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1", "c2"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c2", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
{:add_cell_evaluation_response, @cid, "c2", @input, eval_meta()},
{:set_input_value, @cid, "i1", "value"},
{:queue_cells_evaluation, @cid, ["c1"]}
])
@ -2473,8 +2465,6 @@ defmodule Livebook.Session.DataTest do
end
test "does not garbage collect inputs if another evaluation is ongoing" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -2487,7 +2477,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
{:set_input_value, @cid, "i1", "value"},
{:queue_cells_evaluation, @cid, ["c1"]},
{:queue_cells_evaluation, @cid, ["c2"]}
@ -2601,8 +2591,6 @@ defmodule Livebook.Session.DataTest do
end
test "updates code cell info with binding to the input cell" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -2611,7 +2599,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
{:queue_cells_evaluation, @cid, ["c2"]}
])
@ -3664,8 +3652,6 @@ defmodule Livebook.Session.DataTest do
end
test "stores new input value" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -3673,7 +3659,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()}
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()}
])
operation = {:set_input_value, @cid, "i1", "stuff"}
@ -3683,8 +3669,6 @@ defmodule Livebook.Session.DataTest do
end
test "given input value change, marks evaluated bound cells and their dependents as stale" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -3696,7 +3680,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
evaluate_cells_operations(["c2", "c3", "c4"],
bind_inputs: %{"c3" => ["i1"]},
uses: %{"c2" => ["c1"], "c3" => ["c2"], "c4" => ["c3"]}
@ -4262,8 +4246,6 @@ defmodule Livebook.Session.DataTest do
end
test "does not automatically reevaluate" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!(Data.new(mode: :app), [
{:insert_section, @cid, 0, "s1"},
@ -4273,7 +4255,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
evaluate_cells_operations(["c2"], bind_inputs: %{"c2" => ["i1"]})
])
@ -4291,8 +4273,6 @@ defmodule Livebook.Session.DataTest do
end
test "returns code cells bound to the given input" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -4303,7 +4283,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
evaluate_cells_operations(["c2", "c3", "c4"], %{
bind_inputs: %{"c2" => ["i1"], "c4" => ["i1"]}
})
@ -4480,8 +4460,21 @@ defmodule Livebook.Session.DataTest do
end
test "returns inputs which value changed since they have been bound to some cell" do
input1 = %{id: "i1", type: :text, label: "Text", default: "hey"}
input2 = %{id: "i2", type: :text, label: "Text", default: "hey"}
input1 = %{
type: :input,
ref: "ref1",
id: "i1",
destination: nil,
attrs: %{type: :text, default: "hey", label: "Text"}
}
input2 = %{
type: :input,
ref: "ref2",
id: "i2",
destination: nil,
attrs: %{type: :text, default: "hey", label: "Text"}
}
data =
data_after_operations!([
@ -4493,8 +4486,8 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1", "c2"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input1}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c2", {:input, input2}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", input1, eval_meta()},
{:add_cell_evaluation_response, @cid, "c2", input2, eval_meta()},
evaluate_cells_operations(["c3", "c4"], %{
bind_inputs: %{"c3" => ["i1"], "c4" => ["i2"]}
}),
@ -4505,8 +4498,6 @@ defmodule Livebook.Session.DataTest do
end
test "includes an input where one cell is bound with the old value and one with latest" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -4516,7 +4507,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
evaluate_cells_operations(["c2", "c3"], %{
bind_inputs: %{"c2" => ["i1"], "c3" => ["i1"]}
}),
@ -4529,8 +4520,6 @@ defmodule Livebook.Session.DataTest do
end
test "does not return removed inputs" do
input = %{id: "i1", type: :text, label: "Text", default: "hey"}
data =
data_after_operations!([
{:insert_section, @cid, 0, "s1"},
@ -4539,7 +4528,7 @@ defmodule Livebook.Session.DataTest do
{:set_runtime, @cid, connected_noop_runtime()},
evaluate_cells_operations(["setup"]),
{:queue_cells_evaluation, @cid, ["c1"]},
{:add_cell_evaluation_response, @cid, "c1", {:input, input}, eval_meta()},
{:add_cell_evaluation_response, @cid, "c1", @input, eval_meta()},
evaluate_cells_operations(["c2"], %{bind_inputs: %{"c2" => ["i1"]}}),
{:set_input_value, @cid, "i1", "new value"},
{:delete_cell, @cid, "c1"}

View file

@ -187,7 +187,7 @@ defmodule Livebook.SessionTest do
| kind: "text",
source: "chunk 1\n\nchunk 2",
chunks: [{0, 7}, {9, 7}],
outputs: [{1, {:terminal_text, "Hello", %{chunk: false}}}]
outputs: [{1, terminal_text("Hello")}]
}
section = %{Notebook.Section.new() | cells: [smart_cell]}
@ -210,24 +210,23 @@ defmodule Livebook.SessionTest do
assert_receive {:operation,
{:insert_cell, _client_id, ^section_id, 1, :code, _id,
%{source: "chunk 2", outputs: [{1, {:terminal_text, "Hello", %{}}}]}}}
%{source: "chunk 2", outputs: [{1, terminal_text("Hello")}]}}}
end
test "doesn't garbage collect input values" do
input = %{
ref: :input_ref,
type: :input,
ref: "ref",
id: "input1",
type: :text,
label: "Name",
default: "hey",
destination: :noop
destination: :noop,
attrs: %{type: :text, default: "hey", label: "Name"}
}
smart_cell = %{
Notebook.Cell.new(:smart)
| kind: "text",
source: "content",
outputs: [{1, {:input, input}}]
outputs: [{1, input}]
}
section = %{Notebook.Section.new() | cells: [smart_cell]}
@ -648,16 +647,14 @@ defmodule Livebook.SessionTest do
test "schedules file for deletion when the corresponding input is removed",
%{tmp_dir: tmp_dir} do
input = %{
ref: :input_ref,
type: :input,
ref: "ref",
id: "input1",
type: :file,
label: "File",
default: nil,
destination: :noop,
accept: :any
attrs: %{type: :file, accept: :any, default: nil, label: "File"}
}
cell = %{Notebook.Cell.new(:code) | outputs: [{1, {:input, input}}]}
cell = %{Notebook.Cell.new(:code) | outputs: [{1, input}]}
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [cell]}]}
session = start_session(notebook: notebook, registered_file_deletion_delay: 0)
@ -882,11 +879,17 @@ defmodule Livebook.SessionTest do
# between runtime and session
@livebook_put_input_code """
input = %{id: "input1", type: :number, label: "Name", default: "hey"}
input = %{
type: :input,
ref: "ref",
id: "input1",
destination: nil,
attrs: %{type: :number, default: "hey", label: "Name"}
}
send(
Process.group_leader(),
{:io_request, self(), make_ref(), {:livebook_put_output, {:input, input}}}
{:io_request, self(), make_ref(), {:livebook_put_output, input}}
)
"""
@ -920,8 +923,8 @@ defmodule Livebook.SessionTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, text_output, %{}}, %{evaluation_time_ms: _time_ms}}}
{:add_cell_evaluation_response, _, ^cell_id, terminal_text(text_output),
%{evaluation_time_ms: _time_ms}}}
assert text_output =~ "hey"
end
@ -944,8 +947,8 @@ defmodule Livebook.SessionTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, text_output, %{}}, %{evaluation_time_ms: _time_ms}}}
{:add_cell_evaluation_response, _, ^cell_id, terminal_text(text_output),
%{evaluation_time_ms: _time_ms}}}
assert text_output =~ ":error"
end
@ -1242,8 +1245,8 @@ defmodule Livebook.SessionTest do
archive_path = Path.expand("../support/assets.tar.gz", __DIR__)
hash = "test-" <> Utils.random_id()
assets_info = %{archive_path: archive_path, hash: hash, js_path: "main.js"}
js_output = {:js, %{js_view: %{assets: assets_info}}}
frame_output = {:frame, [js_output], %{ref: "1", type: :replace}}
js_output = %{type: :js, js_view: %{assets: assets_info}, export: nil}
frame_output = %{type: :frame, ref: "1", outputs: [js_output], placeholder: true}
user = Livebook.Users.User.new()
{_, client_id} = Session.register_client(session.pid, self(), user)
@ -1856,6 +1859,53 @@ defmodule Livebook.SessionTest do
end
end
test "supports legacy text outputs" do
session = start_session()
Session.subscribe(session.id)
{_section_id, cell_id} = insert_section_and_cell(session.pid)
runtime = connected_noop_runtime()
Session.set_runtime(session.pid, runtime)
user = Livebook.Users.User.new()
Session.register_client(session.pid, self(), user)
legacy_output = {:text, "Hola"}
expected_output = terminal_text("Hola")
send(session.pid, {:runtime_evaluation_output, cell_id, legacy_output})
assert_receive {:operation, {:add_cell_evaluation_output, _, ^cell_id, ^expected_output}}
legacy_output = {:markdown, "Hola"}
expected_output = %{type: :markdown, text: "Hola", chunk: false}
send(session.pid, {:runtime_evaluation_output, cell_id, legacy_output})
assert_receive {:operation, {:add_cell_evaluation_output, _, ^cell_id, ^expected_output}}
legacy_output =
{:input,
%{
type: :text,
ref: "ref",
id: "input1",
label: "Name",
default: "hey",
destination: :noop
}}
expected_output =
%{
type: :input,
ref: "ref",
id: "input1",
destination: :noop,
attrs: %{type: :text, default: "hey", label: "Name"}
}
send(session.pid, {:runtime_evaluation_output, cell_id, legacy_output})
assert_receive {:operation, {:add_cell_evaluation_output, _, ^cell_id, ^expected_output}}
end
defp start_session(opts \\ []) do
opts = Keyword.merge([id: Utils.random_id()], opts)
pid = start_supervised!({Session, opts}, id: opts[:id])

View file

@ -207,8 +207,8 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:terminal_text, output, %{}},
_}}
{:add_cell_evaluation_response, _, ^cell_id,
%{type: :terminal_text, text: output}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
@ -272,8 +272,8 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:terminal_text, output, %{}},
_}}
{:add_cell_evaluation_response, _, ^cell_id,
%{type: :terminal_text, text: output}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end

View file

@ -221,7 +221,7 @@ defmodule LivebookWeb.SessionControllerTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
outputs: [{0, %{type: :terminal_text, text: "hey", chunk: true}}]
}
]
}
@ -373,7 +373,7 @@ defmodule LivebookWeb.SessionControllerTest do
archive_path = Path.expand("../../support/assets.tar.gz", __DIR__)
hash = "test-" <> Livebook.Utils.random_id()
assets_info = %{archive_path: archive_path, hash: hash, js_path: "main.js"}
output = {:js, %{js_view: %{assets: assets_info}}}
output = %{type: :js, js_view: %{assets: assets_info}, export: nil}
notebook = %{
Notebook.new()

View file

@ -81,11 +81,17 @@ defmodule LivebookWeb.AppSessionLiveTest do
| cells: [
%{
Livebook.Notebook.Cell.new(:code)
| source: source_for_output({:terminal_text, "Printed output", %{chunk: false}})
| source:
source_for_output(%{
type: :terminal_text,
text: "Printed output",
chunk: false
})
},
%{
Livebook.Notebook.Cell.new(:code)
| source: source_for_output({:plain_text, "Custom text", %{chunk: false}})
| source:
source_for_output(%{type: :plain_text, text: "Custom text", chunk: false})
}
]
}
@ -121,7 +127,12 @@ defmodule LivebookWeb.AppSessionLiveTest do
| cells: [
%{
Livebook.Notebook.Cell.new(:code)
| source: source_for_output({:terminal_text, "Printed output", %{chunk: false}})
| source:
source_for_output(%{
type: :terminal_text,
text: "Printed output",
chunk: false
})
},
%{
Livebook.Notebook.Cell.new(:code)
@ -174,12 +185,11 @@ defmodule LivebookWeb.AppSessionLiveTest do
Process.register(self(), test)
input = %{
ref: :input_ref,
type: :input,
ref: "ref1",
id: "input1",
type: :number,
label: "Name",
default: 1,
destination: test
destination: test,
attrs: %{type: :number, default: 1, label: "Name"}
}
notebook = %{
@ -191,7 +201,7 @@ defmodule LivebookWeb.AppSessionLiveTest do
| cells: [
%{
Livebook.Notebook.Cell.new(:code)
| source: source_for_output({:input, input})
| source: source_for_output(input)
},
%{
Livebook.Notebook.Cell.new(:code)

View file

@ -177,7 +177,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[32m\"true\"\e[0m", %{chunk: false}}, _}}
terminal_text("\e[32m\"true\"\e[0m"), _}}
end
test "cancelling cell evaluation", %{conn: conn, session: session} do
@ -486,17 +486,16 @@ defmodule LivebookWeb.SessionLiveTest do
Process.register(self(), test)
input = %{
ref: :input_ref,
type: :input,
ref: "ref1",
id: "input1",
type: :number,
label: "Name",
default: 1,
destination: test
destination: test,
attrs: %{type: :number, default: 1, label: "Name"}
}
Session.subscribe(session.id)
insert_cell_with_output(session.pid, section_id, {:input, input})
insert_cell_with_output(session.pid, section_id, input)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
@ -506,7 +505,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert %{input_infos: %{"input1" => %{value: 10}}} = Session.get_data(session.pid)
assert_receive {:event, :input_ref, %{value: 10, type: :change}}
assert_receive {:event, "ref1", %{value: 10, type: :change}}
end
test "newlines in text input are normalized", %{conn: conn, session: session, test: test} do
@ -515,17 +514,16 @@ defmodule LivebookWeb.SessionLiveTest do
Process.register(self(), test)
input = %{
ref: :input_ref,
type: :input,
ref: "ref1",
id: "input1",
type: :textarea,
label: "Name",
default: "hey",
destination: test
destination: test,
attrs: %{type: :textarea, default: "hey", label: "Name", monospace: false}
}
Session.subscribe(session.id)
insert_cell_with_output(session.pid, section_id, {:input, input})
insert_cell_with_output(session.pid, section_id, input)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
@ -543,27 +541,29 @@ defmodule LivebookWeb.SessionLiveTest do
Process.register(self(), test)
form_control = %{
type: :form,
ref: :form_ref,
type: :control,
ref: "control_ref1",
destination: test,
fields: [
name: %{
ref: :input_ref,
id: "input1",
type: :text,
label: "Name",
default: "initial",
destination: test
}
],
submit: "Send",
report_changes: %{},
reset_on_submit: []
attrs: %{
type: :form,
fields: [
name: %{
type: :input,
ref: "input_ref1",
id: "input1",
destination: test,
attrs: %{type: :text, default: "initial", label: "Name"}
}
],
submit: "Send",
report_changes: %{},
reset_on_submit: []
}
}
Session.subscribe(session.id)
insert_cell_with_output(session.pid, section_id, {:control, form_control})
insert_cell_with_output(session.pid, section_id, form_control)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
@ -580,7 +580,7 @@ defmodule LivebookWeb.SessionLiveTest do
|> element(~s/[data-el-outputs-container] button/, "Send")
|> render_click()
assert_receive {:event, :form_ref, %{data: %{name: "sherlock"}, type: :submit}}
assert_receive {:event, "control_ref1", %{data: %{name: "sherlock"}, type: :submit}}
end
test "file input", %{conn: conn, session: session, test: test} do
@ -589,18 +589,16 @@ defmodule LivebookWeb.SessionLiveTest do
Process.register(self(), test)
input = %{
ref: :input_ref,
type: :input,
ref: "ref1",
id: "input1",
type: :file,
label: "File",
default: nil,
destination: test,
accept: :any
attrs: %{type: :file, default: nil, label: "File", accept: :any}
}
Session.subscribe(session.id)
insert_cell_with_output(session.pid, section_id, {:input, input})
insert_cell_with_output(session.pid, section_id, input)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
@ -636,18 +634,12 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
send(
session.pid,
{:runtime_evaluation_output, cell_id, {:terminal_text, "line 1\n", %{chunk: true}}}
)
send(session.pid, {:runtime_evaluation_output, cell_id, terminal_text("line 1\n", true)})
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
assert render(view) =~ "line 1"
send(
session.pid,
{:runtime_evaluation_output, cell_id, {:terminal_text, "line 2\n", %{chunk: true}}}
)
send(session.pid, {:runtime_evaluation_output, cell_id, terminal_text("line 2\n", true)})
wait_for_session_update(session.pid)
# Render once, so that the send_update is processed
@ -664,21 +656,19 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
send(
session.pid,
{:runtime_evaluation_output, cell_id,
{:frame, [{:terminal_text, "In frame", %{chunk: false}}], %{ref: "1", type: :default}}}
)
frame = %{type: :frame, ref: "1", outputs: [terminal_text("In frame")], placeholder: true}
send(session.pid, {:runtime_evaluation_output, cell_id, frame})
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
assert render(view) =~ "In frame"
send(
session.pid,
{:runtime_evaluation_output, cell_id,
{:frame, [{:terminal_text, "Updated frame", %{chunk: false}}],
%{ref: "1", type: :replace}}}
)
frame_update = %{
type: :frame_update,
ref: "1",
update: {:replace, [terminal_text("Updated frame")]}
}
send(session.pid, {:runtime_evaluation_output, cell_id, frame_update})
wait_for_session_update(session.pid)
@ -704,13 +694,11 @@ defmodule LivebookWeb.SessionLiveTest do
send(
session.pid,
{:runtime_evaluation_output_to, client_id, cell_id,
{:terminal_text, "line 1\n", %{chunk: true}}}
{:runtime_evaluation_output_to, client_id, cell_id, terminal_text("line 1\n", true)}
)
assert_receive {:operation,
{:add_cell_evaluation_output, _, ^cell_id,
{:terminal_text, "line 1\n", %{chunk: true}}}}
{:add_cell_evaluation_output, _, ^cell_id, terminal_text("line 1\n", true)}}
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
refute render(view) =~ "line 1"
@ -731,13 +719,11 @@ defmodule LivebookWeb.SessionLiveTest do
send(
session.pid,
{:runtime_evaluation_output_to_clients, cell_id,
{:terminal_text, "line 1\n", %{chunk: false}}}
{:runtime_evaluation_output_to_clients, cell_id, terminal_text("line 1\n")}
)
assert_receive {:operation,
{:add_cell_evaluation_output, _, ^cell_id,
{:terminal_text, "line 1\n", %{chunk: false}}}}
{:add_cell_evaluation_output, _, ^cell_id, terminal_text("line 1\n")}}
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
refute render(view) =~ "line 1"
@ -750,17 +736,16 @@ defmodule LivebookWeb.SessionLiveTest do
Process.register(self(), test)
input = %{
ref: :input_ref,
type: :input,
ref: "ref1",
id: "input1",
type: :number,
label: "Name",
default: 1,
destination: test
destination: test,
attrs: %{type: :number, default: 1, label: "Name"}
}
Session.subscribe(session.id)
insert_cell_with_output(session.pid, section_id, {:input, input})
insert_cell_with_output(session.pid, section_id, input)
code = source_for_input_read(input.id)
cell_id = insert_text_cell(session.pid, section_id, :code, code)
@ -1479,8 +1464,7 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
{:add_cell_evaluation_response, _, ^cell_id, terminal_text(output), _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
@ -1527,8 +1511,7 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
{:add_cell_evaluation_response, _, ^cell_id, terminal_text(output), _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
@ -1620,7 +1603,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[35mnil\e[0m", %{chunk: false}}, _}}
terminal_text("\e[35mnil\e[0m"), _}}
attrs = params_for(:env_var, name: "MY_AWESOME_ENV", value: "MyEnvVarValue")
Settings.set_env_var(attrs)
@ -1631,7 +1614,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[32m\"MyEnvVarValue\"\e[0m", %{chunk: false}}, _}}
terminal_text("\e[32m\"MyEnvVarValue\"\e[0m"), _}}
Settings.set_env_var(%{attrs | value: "OTHER_VALUE"})
@ -1641,7 +1624,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[32m\"OTHER_VALUE\"\e[0m", %{chunk: false}}, _}}
terminal_text("\e[32m\"OTHER_VALUE\"\e[0m"), _}}
Settings.unset_env_var("MY_AWESOME_ENV")
@ -1651,7 +1634,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[35mnil\e[0m", %{chunk: false}}, _}}
terminal_text("\e[35mnil\e[0m"), _}}
end
@tag :tmp_dir
@ -1685,8 +1668,7 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
{:add_cell_evaluation_response, _, ^cell_id, terminal_text(output), _}}
assert output == "\e[32m\"#{String.replace(expected_path, "\\", "\\\\")}\"\e[0m"
@ -1697,8 +1679,7 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
{:add_cell_evaluation_response, _, ^cell_id, terminal_text(output), _}}
assert output == "\e[32m\"#{String.replace(initial_os_path, "\\", "\\\\")}\"\e[0m"
end
@ -2031,7 +2012,7 @@ defmodule LivebookWeb.SessionLiveTest do
insert_cell_with_output(
session.pid,
section_id,
{:terminal_text, "Hello from the app!", %{chunk: false}}
terminal_text("Hello from the app!")
)
slug = Livebook.Utils.random_short_id()

View file

@ -112,4 +112,13 @@ defmodule Livebook.TestHelpers do
{code, ack_fun}
end
@doc """
Builds a terminal output map.
"""
defmacro terminal_text(text, chunk \\ false) do
quote do
%{type: :terminal_text, text: unquote(text), chunk: unquote(chunk)}
end
end
end