mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-12-09 05:05:55 +08:00
Restructure output format (#2179)
This commit is contained in:
parent
f2795c7067
commit
da9cc6643b
28 changed files with 1132 additions and 688 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: []}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
_ ->
|
||||
|
|
|
|||
|
|
@ -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)}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue