Add support for chunked text and markdown outputs (#2174)

This commit is contained in:
Jonatan Kłosko 2023-08-22 13:21:22 +02:00 committed by GitHub
parent 874155db15
commit a11b1dfe7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 528 additions and 267 deletions

View file

@ -1,35 +1,47 @@
import { getAttributeOrThrow } from "../lib/attribute";
import Markdown from "../lib/markdown";
import { findChildOrThrow } from "../lib/utils";
/**
* A hook used to render Markdown content on the client.
*
* ## Configuration
*
* * `data-id` - id of the renderer, under which the content event
* is pushed
* * `data-base-path` - the path to resolve relative URLs against
*
* * `data-allowed-uri-schemes` - a comma separated list of additional
* URI schemes that should be kept during sanitization
*
* The element should have two children:
*
* * `[data-template]` - a hidden container containing the markdown
* content. The DOM structure is ignored, only text content matters
*
* * `[data-content]` - the target element to render results into
*
*/
const MarkdownRenderer = {
mounted() {
this.props = this.getProps();
const markdown = new Markdown(this.el, "", {
baseUrl: this.props.sessionPath,
this.templateEl = findChildOrThrow(this.el, "[data-template]");
this.contentEl = findChildOrThrow(this.el, "[data-content]");
this.markdown = new Markdown(this.contentEl, this.templateEl.textContent, {
baseUrl: this.props.basePath,
allowedUriSchemes: this.props.allowedUriSchemes.split(","),
});
},
this.handleEvent(
`markdown_renderer:${this.props.id}:content`,
({ content }) => {
markdown.setContent(content);
}
);
updated() {
this.props = this.getProps();
this.markdown.setContent(this.templateEl.textContent);
},
getProps() {
return {
id: getAttributeOrThrow(this.el, "data-id"),
sessionPath: getAttributeOrThrow(this.el, "data-session-path"),
basePath: getAttributeOrThrow(this.el, "data-base-path"),
allowedUriSchemes: getAttributeOrThrow(
this.el,
"data-allowed-uri-schemes"

View file

@ -212,7 +212,7 @@ defmodule Livebook.LiveMarkdown.Export do
|> Enum.intersperse("\n\n")
end
defp render_output({:stdout, text}, _ctx) do
defp render_output({:terminal_text, text, %{}}, _ctx) do
text = String.replace_suffix(text, "\n", "")
delimiter = MarkdownHelpers.code_block_delimiter(text)
text = strip_ansi(text)
@ -221,14 +221,6 @@ defmodule Livebook.LiveMarkdown.Export do
|> prepend_metadata(%{output: true})
end
defp render_output({:text, text}, _ctx) do
delimiter = MarkdownHelpers.code_block_delimiter(text)
text = strip_ansi(text)
[delimiter, "\n", text, "\n", delimiter]
|> prepend_metadata(%{output: true})
end
defp render_output(
{:js, %{export: %{info_string: info_string, key: key}, js_view: %{ref: ref}}},
ctx

View file

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

View file

@ -666,9 +666,21 @@ defmodule Livebook.Notebook do
@doc """
Adds new output to the given cell.
Automatically merges stdout outputs and updates frames.
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}
@ -714,45 +726,80 @@ defmodule Livebook.Notebook do
end)
end
defp apply_frame_update(_outputs, new_outputs, :replace), do: new_outputs
defp apply_frame_update(outputs, new_outputs, :append), do: new_outputs ++ outputs
defp apply_frame_update(_outputs, new_outputs, :replace) do
merge_chunk_outputs(new_outputs)
end
defp apply_frame_update(outputs, new_outputs, :append) do
Enum.reduce(Enum.reverse(new_outputs), outputs, &add_output(&2, &1))
end
defp add_output(outputs, {_idx, :ignored}), do: outputs
defp add_output([], {idx, {:stdout, text}}),
do: [{idx, {:stdout, normalize_stdout(text)}}]
defp add_output([], output), do: [output]
# Session clients prune stdout content and handle subsequent
# ones by directly appending page content to the previous one
defp add_output([{_idx1, {:stdout, :__pruned__}} | _] = outputs, {_idx2, {:stdout, _text}}) do
outputs
# Session clients prune rendered chunks, we only keep add the new one
defp add_output(
[{idx, {type, :__pruned__, %{chunk: true} = info}} | tail],
{_idx, {type, text, %{chunk: true}}}
)
when type in [:terminal_text, :plain_text, :markdown] do
[{idx, {type, text, info}} | tail]
end
# Session server keeps all outputs, so we merge consecutive stdouts
defp add_output([{idx, {:stdout, text}} | tail], {_idx, {:stdout, cont}}) do
[{idx, {:stdout, normalize_stdout(text <> cont)}} | tail]
# 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}}}
) do
[{idx, {:terminal_text, normalize_terminal_text(text <> cont), info}} | tail]
end
defp add_output(outputs, {idx, {:terminal_text, text, info}}) do
[{idx, {:terminal_text, normalize_terminal_text(text), info}} | outputs]
end
defp add_output(
[{idx, {type, text, %{chunk: true} = info}} | tail],
{_idx, {type, cont, %{chunk: true}}}
)
when type in [:plain_text, :markdown] do
[{idx, {type, normalize_terminal_text(text <> cont), info}} | 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]
end
defp add_output(outputs, output), do: [output | outputs]
@doc """
Normalizes a text chunk coming form the standard output.
Handles CR rawinds and caps output lines.
"""
@spec normalize_stdout(String.t()) :: String.t()
def normalize_stdout(text) do
text
|> Livebook.Utils.apply_rewind()
|> Livebook.Utils.cap_lines(max_stdout_lines())
defp merge_chunk_outputs(outputs) do
outputs
|> Enum.reverse()
|> Enum.reduce([], &add_output(&2, &1))
end
@doc """
The maximum desired number of lines of the standard output.
Normalizes terminal text chunk.
Handles CR rewinds and caps output lines.
"""
def max_stdout_lines(), do: 1_000
@spec normalize_terminal_text(String.t()) :: String.t()
def normalize_terminal_text(text) do
text
|> Livebook.Utils.apply_rewind()
|> Livebook.Utils.cap_lines(max_terminal_lines())
end
@doc """
The maximum desired number of lines of terminal text.
This is particularly relevant for standard output, which may receive
a lot of lines.
"""
def max_terminal_lines(), do: 1_000
@doc """
Recursively adds index to all outputs, including frames.
@ -803,60 +850,65 @@ defmodule Livebook.Notebook do
@spec prune_cell_outputs(t()) :: t()
def prune_cell_outputs(notebook) do
update_cells(notebook, fn
%{outputs: _outputs} = cell -> %{cell | outputs: prune_outputs(cell.outputs)}
%{outputs: _outputs} = cell -> %{cell | outputs: prune_outputs(cell.outputs, true)}
cell -> cell
end)
end
defp prune_outputs(outputs) do
defp prune_outputs(outputs, appendable?) do
outputs
|> Enum.reverse()
|> do_prune_outputs([])
|> do_prune_outputs(appendable?, [])
end
defp do_prune_outputs([], acc), do: acc
defp do_prune_outputs([], _appendable?, acc), do: acc
# Keep the last stdout, so that we know to message it directly, but remove its contents
defp do_prune_outputs([{idx, {:stdout, _}}], acc) do
[{idx, {:stdout, :__pruned__}} | acc]
# Keep trailing outputs that can be merged with subsequent outputs
defp do_prune_outputs([{idx, {type, _, %{chunk: true} = info}}], true = _appendable?, acc)
when type in [:terminal_text, :plain_text, :markdown] do
[{idx, {type, :__pruned__, info}} | acc]
end
# Keep frame and its relevant contents
defp do_prune_outputs([{idx, {:frame, frame_outputs, info}} | outputs], acc) do
do_prune_outputs(outputs, [{idx, {:frame, prune_outputs(frame_outputs), info}} | acc])
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]
)
end
# Keep layout output and its relevant contents
defp do_prune_outputs([{idx, {:tabs, tabs_outputs, info}} | outputs], acc) do
case prune_outputs(tabs_outputs) do
defp do_prune_outputs([{idx, {:tabs, tabs_outputs, info}} | outputs], appendable?, acc) do
case prune_outputs(tabs_outputs, false) do
[] ->
do_prune_outputs(outputs, acc)
do_prune_outputs(outputs, appendable?, acc)
pruned_tabs_outputs ->
info = Map.replace(info, :labels, :__pruned__)
do_prune_outputs(outputs, [{idx, {:tabs, pruned_tabs_outputs, info}} | acc])
do_prune_outputs(outputs, appendable?, [{idx, {:tabs, pruned_tabs_outputs, info}} | acc])
end
end
defp do_prune_outputs([{idx, {:grid, grid_outputs, info}} | outputs], acc) do
case prune_outputs(grid_outputs) do
defp do_prune_outputs([{idx, {:grid, grid_outputs, info}} | outputs], appendable?, acc) do
case prune_outputs(grid_outputs, false) do
[] ->
do_prune_outputs(outputs, acc)
do_prune_outputs(outputs, appendable?, acc)
pruned_grid_outputs ->
do_prune_outputs(outputs, [{idx, {:grid, pruned_grid_outputs, info}} | acc])
do_prune_outputs(outputs, appendable?, [{idx, {:grid, pruned_grid_outputs, info}} | acc])
end
end
# Keep outputs that get re-rendered
defp do_prune_outputs([{idx, output} | outputs], acc)
defp do_prune_outputs([{idx, output} | outputs], appendable?, acc)
when elem(output, 0) in [:input, :control, :error] do
do_prune_outputs(outputs, [{idx, output} | acc])
do_prune_outputs(outputs, appendable?, [{idx, output} | acc])
end
# Remove everything else
defp do_prune_outputs([_output | outputs], acc) do
do_prune_outputs(outputs, acc)
defp do_prune_outputs([_output | outputs], appendable?, acc) do
do_prune_outputs(outputs, appendable?, acc)
end
@doc """

View file

@ -72,14 +72,12 @@ defprotocol Livebook.Runtime do
"""
@type output ::
:ignored
# IO output, adjacent such outputs are treated as a whole
| {:stdout, binary()}
# Standalone text block otherwise matching :stdout
| {:text, binary()}
# Text with terminal style and ANSI support
| {:terminal_text, text :: String.t(), info :: map()}
# Plain text content
| {:plain_text, binary()}
| {:plain_text, text :: String.t(), info :: map()}
# Markdown content
| {:markdown, binary()}
| {:markdown, text :: String.t(), info :: map()}
# A raw image in the given format
| {:image, content :: binary(), mime_type :: binary()}
# JavaScript powered output

View file

@ -76,7 +76,7 @@ defmodule Livebook.Runtime.Evaluator.Formatter do
defp to_inspect_output(value, opts \\ []) do
try do
inspected = inspect(value, inspect_opts(opts))
{:text, inspected}
{:terminal_text, inspected, %{chunk: false}}
catch
kind, error ->
formatted = format_error(kind, error, __STACKTRACE__)
@ -172,6 +172,6 @@ defmodule Livebook.Runtime.Evaluator.Formatter do
defp erlang_to_output(value) do
text = :io_lib.format("~p", [value]) |> IO.iodata_to_binary()
{:text, text}
{:terminal_text, text, %{chunk: false}}
end
end

View file

@ -438,7 +438,10 @@ 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, {:stdout, string}})
send(
state.send_to,
{:runtime_evaluation_output, state.ref, {:terminal_text, string, %{chunk: true}}}
)
end
%{state | buffer: []}

View file

@ -37,40 +37,26 @@ defmodule LivebookWeb.Output do
"""
end
defp border?({:stdout, _text}), do: true
defp border?({:text, _text}), do: true
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?(_output), do: false
defp render_output({:stdout, text}, %{id: id}) do
defp render_output({:terminal_text, text, _info}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
live_component(Output.StdoutComponent, id: id, text: text)
live_component(Output.TerminalTextComponent, id: id, text: text)
end
defp render_output({:text, text}, %{id: id}) do
assigns = %{id: id, text: text}
~H"""
<Output.TextComponent.render id={@id} content={@text} />
"""
defp render_output({:plain_text, text, _info}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
live_component(Output.PlainTextComponent, id: id, text: text)
end
defp render_output({:plain_text, text}, %{id: id}) do
assigns = %{id: id, text: text}
~H"""
<div id={@id} class="text-gray-700 whitespace-pre-wrap"><%= @text %></div>
"""
end
defp render_output({:markdown, markdown}, %{id: id, session_id: session_id}) do
live_component(Output.MarkdownComponent,
id: id,
session_id: session_id,
content: markdown
)
defp render_output({:markdown, text, _info}, %{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

View file

@ -3,30 +3,41 @@ defmodule LivebookWeb.Output.MarkdownComponent do
@impl true
def mount(socket) do
{:ok, assign(socket, allowed_uri_schemes: Livebook.Config.allowed_uri_schemes())}
{:ok, assign(socket, allowed_uri_schemes: Livebook.Config.allowed_uri_schemes(), chunks: 0),
temporary_assigns: [text: nil]}
end
@impl true
def update(assigns, socket) do
{text, assigns} = Map.pop(assigns, :text)
socket = assign(socket, assigns)
{:ok,
push_event(socket, "markdown_renderer:#{socket.assigns.id}:content", %{
content: socket.assigns.content
})}
if text do
{:ok, socket |> assign(text: text) |> update(:chunks, &(&1 + 1))}
else
{:ok, socket}
end
end
@impl true
def render(assigns) do
~H"""
<div
class="markdown"
id={"markdown-renderer-#{@id}"}
phx-hook="MarkdownRenderer"
data-id={@id}
data-session-path={~p"/sessions/#{@session_id}"}
data-base-path={~p"/sessions/#{@session_id}"}
data-allowed-uri-schemes={Enum.join(@allowed_uri_schemes, ",")}
>
<div
data-template
id={"markdown-renderer-#{@id}-template"}
class="text-gray-700 whitespace-pre-wrap hidden"
phx-update="append"
phx-no-format
><span :if={@text} id={"plain-text-#{@id}-chunk-#{@chunks}"}><%=
@text %></span></div>
<div data-content class="markdown" id={"markdown-rendered-#{@id}-content"} phx-update="ignore">
</div>
</div>
"""
end

View file

@ -0,0 +1,33 @@
defmodule LivebookWeb.Output.PlainTextComponent do
use LivebookWeb, :live_component
@impl true
def mount(socket) do
{:ok, assign(socket, chunks: 0), temporary_assigns: [text: nil]}
end
@impl true
def update(assigns, socket) do
{text, assigns} = Map.pop(assigns, :text)
socket = assign(socket, assigns)
if text do
{:ok, socket |> assign(text: text) |> update(:chunks, &(&1 + 1))}
else
{:ok, socket}
end
end
@impl true
def render(assigns) do
~H"""
<div
id={"plain-text-#{@id}"}
class="text-gray-700 whitespace-pre-wrap"
phx-update="append"
phx-no-format
><span :if={@text} id={"plain-text-#{@id}-chunk-#{@chunks}"}><%=
@text %></span></div>
"""
end
end

View file

@ -1,4 +1,4 @@
defmodule LivebookWeb.Output.StdoutComponent do
defmodule LivebookWeb.Output.TerminalTextComponent do
use LivebookWeb, :live_component
@impl true
@ -15,7 +15,7 @@ defmodule LivebookWeb.Output.StdoutComponent do
if text do
text = (socket.assigns.last_line || "") <> text
text = Livebook.Notebook.normalize_stdout(text)
text = Livebook.Notebook.normalize_terminal_text(text)
last_line =
case Livebook.Utils.split_at_last_occurrence(text, "\n") do
@ -49,7 +49,7 @@ defmodule LivebookWeb.Output.StdoutComponent do
phx-hook="VirtualizedLines"
data-max-height="300"
data-follow="true"
data-max-lines={Livebook.Notebook.max_stdout_lines()}
data-max-lines={Livebook.Notebook.max_terminal_lines()}
data-ignore-trailing-empty-line="true"
>
<% # Note 1: We add a newline to each element, so that multiple lines can be copied properly as element.textContent %>

View file

@ -1,38 +0,0 @@
defmodule LivebookWeb.Output.TextComponent do
use LivebookWeb, :live_component
@impl true
def render(assigns) do
~H"""
<div
id={"virtualized-text-#{@id}"}
class="relative"
phx-hook="VirtualizedLines"
data-max-height="300"
>
<% # Add a newline to each element, so that multiple lines can be copied properly %>
<div data-template class="hidden" id={"virtualized-text-#{@id}-template"} phx-no-format><%= for line <- ansi_string_to_html_lines(@content) do %><div data-line><%= [
line,
"\n"
] %></div><% end %></div>
<div
data-content
class="overflow-auto whitespace-pre font-editor text-gray-500 tiny-scrollbar"
id={"virtualized-text-#{@id}-content"}
phx-update="ignore"
phx-no-format
>
</div>
<div class="absolute right-2 top-0 z-10">
<button
class="icon-button bg-gray-100"
data-el-clipcopy
phx-click={JS.dispatch("lb:clipcopy", to: "#virtualized-text-#{@id}-template")}
>
<.remix_icon icon="clipboard-line" class="text-lg" />
</button>
</div>
</div>
"""
end
end

View file

@ -2783,11 +2783,19 @@ defmodule LivebookWeb.SessionLive do
data_view
{:add_cell_evaluation_output, _client_id, cell_id, {:stdout, text}} ->
{:add_cell_evaluation_output, _client_id, cell_id, {type, text, %{chunk: true}}}
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, {:stdout, _}} | _]}, _section} ->
send_update(LivebookWeb.Output.StdoutComponent, id: "output-#{idx}", text: text)
{:ok, %{outputs: [{idx, {^type, _, %{chunk: true}}} | _]}, _section} ->
module =
case type do
:terminal_text -> LivebookWeb.Output.TerminalTextComponent
:plain_text -> LivebookWeb.Output.PlainTextComponent
:markdown -> LivebookWeb.Output.MarkdownComponent
end
send_update(module, id: "output-#{idx}", text: text)
data_view
_ ->

View file

@ -577,7 +577,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
IO.puts("hey")\
""",
outputs: [
{0, {:stdout, "hey"}}
{0, {:terminal_text, "hey", %{chunk: true}}}
]
}
]
@ -614,7 +614,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:stdout, "hey"}}]
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
}
]
}
@ -656,7 +656,10 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:text, "\e[34m:ok\e[0m"}}, {1, {:stdout, "hey"}}]
outputs: [
{0, {:terminal_text, "\e[34m:ok\e[0m", %{chunk: false}}},
{1, {:terminal_text, "hey", %{chunk: true}}}
]
}
]
}
@ -947,8 +950,8 @@ defmodule Livebook.LiveMarkdown.ExportTest do
{:tabs,
[
{1, {:markdown, "a"}},
{2, {:text, "b"}},
{3, {:text, "c"}}
{2, {:terminal_text, "b", %{chunk: false}}},
{3, {:terminal_text, "c", %{chunk: false}}}
], %{labels: ["A", "B", "C"]}}}
]
}
@ -994,9 +997,9 @@ defmodule Livebook.LiveMarkdown.ExportTest do
{0,
{:grid,
[
{1, {:text, "a"}},
{1, {:terminal_text, "a", %{chunk: false}}},
{2, {:markdown, "b"}},
{3, {:text, "c"}}
{3, {:terminal_text, "c", %{chunk: false}}}
], %{columns: 2}}}
]
}
@ -1047,7 +1050,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:stdout, "hey"}}]
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
}
]
}
@ -1092,7 +1095,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:stdout, "hey"}}]
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
}
]
}

View file

@ -642,7 +642,10 @@ defmodule Livebook.LiveMarkdown.ImportTest do
source: """
IO.puts("hey")\
""",
outputs: [{0, {:text, ":ok"}}, {1, {:text, "hey"}}]
outputs: [
{0, {:terminal_text, ":ok", %{chunk: false}}},
{1, {:terminal_text, "hey", %{chunk: false}}}
]
}
]
}
@ -882,7 +885,10 @@ defmodule Livebook.LiveMarkdown.ImportTest do
source: """
IO.puts("hey")\
""",
outputs: [{0, {:text, ":ok"}}, {1, {:text, "hey"}}]
outputs: [
{0, {:terminal_text, ":ok", %{chunk: false}}},
{1, {:terminal_text, "hey", %{chunk: false}}}
]
}
]
}

View file

@ -282,7 +282,7 @@ defmodule Livebook.NotebookTest do
end
describe "add_cell_output/3" do
test "merges consecutive stdout results" do
test "merges consecutive text outputs when both are chunks" do
notebook = %{
Notebook.new()
| sections: [
@ -290,7 +290,11 @@ defmodule Livebook.NotebookTest do
Section.new()
| id: "s1",
cells: [
%{Cell.new(:code) | id: "c1", outputs: [{0, {:stdout, "Hola"}}]}
%{
Cell.new(:code)
| id: "c1",
outputs: [{0, {:terminal_text, "Hola", %{chunk: true}}}]
}
]
}
],
@ -300,13 +304,58 @@ defmodule Livebook.NotebookTest do
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:stdout, "Hola amigo!"}}]}]
cells: [%{outputs: [{0, {:terminal_text, "Hola amigo!", %{chunk: true}}}]}]
}
]
} = Notebook.add_cell_output(notebook, "c1", {:stdout, " amigo!"})
} =
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, " amigo!", %{chunk: true}}
)
end
test "normalizes individual stdout results to respect CR" do
test "adds separate text outputs when they are not chunks" do
notebook = %{
Notebook.new()
| sections: [
%{
Section.new()
| id: "s1",
cells: [
%{
Cell.new(:code)
| id: "c1",
outputs: [{0, {:terminal_text, "Hola", %{chunk: false}}}]
}
]
}
],
output_counter: 1
}
assert %{
sections: [
%{
cells: [
%{
outputs: [
{1, {:terminal_text, " amigo!", %{chunk: true}}},
{0, {:terminal_text, "Hola", %{chunk: false}}}
]
}
]
}
]
} =
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, " amigo!", %{chunk: true}}
)
end
test "normalizes individual terminal text outputs to respect CR" do
notebook = %{
Notebook.new()
| sections: [
@ -324,13 +373,18 @@ defmodule Livebook.NotebookTest do
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:stdout, "Hey"}}]}]
cells: [%{outputs: [{0, {:terminal_text, "Hey", %{chunk: false}}}]}]
}
]
} = Notebook.add_cell_output(notebook, "c1", {:stdout, "Hola\rHey"})
} =
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, "Hola\rHey", %{chunk: false}}
)
end
test "normalizes consecutive stdout results to respect CR" do
test "normalizes consecutive terminal text outputs to respect CR" do
notebook = %{
Notebook.new()
| sections: [
@ -338,7 +392,11 @@ defmodule Livebook.NotebookTest do
Section.new()
| id: "s1",
cells: [
%{Cell.new(:code) | id: "c1", outputs: [{0, {:stdout, "Hola"}}]}
%{
Cell.new(:code)
| id: "c1",
outputs: [{0, {:terminal_text, "Hola", %{chunk: true}}}]
}
]
}
],
@ -348,10 +406,15 @@ defmodule Livebook.NotebookTest do
assert %{
sections: [
%{
cells: [%{outputs: [{0, {:stdout, "amigo!\r"}}]}]
cells: [%{outputs: [{0, {:terminal_text, "amigo!\r", %{chunk: true}}}]}]
}
]
} = Notebook.add_cell_output(notebook, "c1", {:stdout, "\ramigo!\r"})
} =
Notebook.add_cell_output(
notebook,
"c1",
{:terminal_text, "\ramigo!\r", %{chunk: true}}
)
end
test "updates existing frames on frame update output" do
@ -384,12 +447,16 @@ defmodule Livebook.NotebookTest do
cells: [
%{
outputs: [
{0, {:frame, [{2, {:text, "hola"}}], %{ref: "1", type: :default}}}
{0,
{:frame, [{2, {:terminal_text, "hola", %{chunk: false}}}],
%{ref: "1", type: :default}}}
]
},
%{
outputs: [
{1, {:frame, [{3, {:text, "hola"}}], %{ref: "1", type: :default}}}
{1,
{:frame, [{3, {:terminal_text, "hola", %{chunk: false}}}],
%{ref: "1", type: :default}}}
]
}
]
@ -399,7 +466,47 @@ defmodule Livebook.NotebookTest do
Notebook.add_cell_output(
notebook,
"c2",
{:frame, [{:text, "hola"}], %{ref: "1", type: :replace}}
{:frame, [{:terminal_text, "hola", %{chunk: false}}],
%{ref: "1", type: :replace}}
)
end
test "merges chunked text outputs in a new frame" do
notebook = %{
Notebook.new()
| sections: [
%{
Section.new()
| id: "s1",
cells: [%{Cell.new(:code) | id: "c1", outputs: []}]
}
],
output_counter: 0
}
assert %{
sections: [
%{
cells: [
%{
outputs: [
{2,
{:frame, [{1, {:terminal_text, "hola amigo!", %{chunk: true}}}],
%{ref: "1", type: :default}}}
]
}
]
}
]
} =
Notebook.add_cell_output(
notebook,
"c1",
{:frame,
[
{:terminal_text, " amigo!", %{chunk: true}},
{:terminal_text, "hola", %{chunk: true}}
], %{ref: "1", type: :default}}
)
end
@ -424,6 +531,36 @@ defmodule Livebook.NotebookTest do
]
} = 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"})
end
end
describe "find_frame_outputs/2" do

View file

@ -63,7 +63,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
[]
)
assert_receive {:runtime_evaluation_output, :e1, {:stdout, output}}
assert_receive {:runtime_evaluation_output, :e1, {:terminal_text, output, %{chunk: true}}}
assert output =~ "error to stdout\n"
end
@ -77,7 +77,9 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, [])
assert_receive {:runtime_evaluation_output, :e1, {:stdout, log_message}}
assert_receive {:runtime_evaluation_output, :e1,
{:terminal_text, log_message, %{chunk: true}}}
assert log_message =~ "[error] hey"
end
@ -87,7 +89,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
RuntimeServer.evaluate_code(pid, :elixir, "x", {:c2, :e2}, [{:c1, :e1}])
assert_receive {:runtime_evaluation_response, :e2, {:text, "\e[34m1\e[0m"},
assert_receive {:runtime_evaluation_response, :e2, {:terminal_text, "\e[34m1\e[0m", %{}},
%{evaluation_time_ms: _time_ms}}
end

View file

@ -18,17 +18,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, {:stdout, "hey\n"}}
assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "hey\n", %{chunk: true}}}
end
test "IO.write", %{io: io} do
IO.write(io, "hey")
assert_receive {:runtime_evaluation_output, :ref, {:stdout, "hey"}}
assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "hey", %{chunk: true}}}
end
test "IO.inspect", %{io: io} do
IO.inspect(io, %{}, [])
assert_receive {:runtime_evaluation_output, :ref, {:stdout, "%{}\n"}}
assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "%{}\n", %{chunk: true}}}
end
test "IO.read", %{io: io} do
@ -83,31 +83,37 @@ defmodule Livebook.Runtime.Evaluator.IOProxyTest do
test "buffers rapid output", %{io: io} do
IO.puts(io, "hey")
IO.puts(io, "hey")
assert_receive {:runtime_evaluation_output, :ref, {:stdout, "hey\nhey\n"}}
assert_receive {:runtime_evaluation_output, :ref,
{:terminal_text, "hey\nhey\n", %{chunk: 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, {:stdout, "\roverride\r"}}
assert_receive {:runtime_evaluation_output, :ref,
{:terminal_text, "\roverride\r", %{chunk: 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, {:stdout, "hey\n"}}
assert_received {:runtime_evaluation_output, :ref, {:terminal_text, "hey\n", %{chunk: true}}}
end
test "supports direct livebook output forwarding", %{io: io} do
livebook_put_output(io, {:text, "[1, 2, 3]"})
livebook_put_output(io, {:terminal_text, "[1, 2, 3]", %{chunk: false}})
assert_received {:runtime_evaluation_output, :ref, {:text, "[1, 2, 3]"}}
assert_received {:runtime_evaluation_output, :ref,
{:terminal_text, "[1, 2, 3]", %{chunk: false}}}
end
test "supports direct livebook output forwarding for a specific client", %{io: io} do
livebook_put_output_to(io, "client1", {:text, "[1, 2, 3]"})
livebook_put_output_to(io, "client1", {:terminal_text, "[1, 2, 3]", %{chunk: false}})
assert_received {:runtime_evaluation_output_to, "client1", :ref, {:text, "[1, 2, 3]"}}
assert_received {:runtime_evaluation_output_to, "client1", :ref,
{:terminal_text, "[1, 2, 3]", %{chunk: false}}}
end
describe "token requests" do

View file

@ -49,8 +49,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {: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
@ -76,13 +76,15 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [:code_1])
assert_receive {:runtime_evaluation_response, :code_2, {: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, {: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
@ -92,10 +94,14 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert_receive {:runtime_evaluation_response, :code_2, _, metadata()}
Evaluator.evaluate_code(evaluator, :elixir, "Process.get(:x)", :code_3, [:code_1])
assert_receive {:runtime_evaluation_response, :code_3, {: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, {: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
@ -103,10 +109,14 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {: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, {:text, result2}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, result2, %{}},
metadata()}
assert result1 != result2
@ -114,16 +124,21 @@ defmodule Livebook.Runtime.EvaluatorTest do
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {: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, {: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, {:stdout, "hey\n"}}
assert_receive {:runtime_evaluation_output, :code_1,
{:terminal_text, "hey\n", %{chunk: true}}}
end
test "using livebook input sends input request to the caller", %{evaluator: evaluator} do
@ -141,7 +156,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, {: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
@ -307,14 +323,18 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code1, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {: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()}
Evaluator.evaluate_code(evaluator, :elixir, code3, :code_3, [:code_2, :code_1])
assert_receive {:runtime_evaluation_response, :code_3, {: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
@ -325,8 +345,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
opts = [file: "/path/dir/file"]
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], opts)
assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_string("/path/dir")},
metadata()}
assert_receive {:runtime_evaluation_response, :code_1,
{:terminal_text, ansi_string("/path/dir"), %{}}, metadata()}
end
test "kills widgets that that no evaluation points to", %{evaluator: evaluator} do
@ -336,8 +356,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {: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)
@ -345,8 +365,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {: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)
@ -367,8 +387,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
[]
)
assert_receive {:runtime_evaluation_response, :code_1, {: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)
@ -384,11 +404,11 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {: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, {:text, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()}
Evaluator.evaluate_code(evaluator, :elixir, code, :code_2, [], file: "file.ex")
@ -418,7 +438,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {: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"))
@ -722,7 +742,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, {:text, _}, metadata}
assert_receive {:runtime_evaluation_response, ^ref, {:terminal_text, _, %{}}, metadata}
%{used: metadata.identifiers_used, defined: metadata.identifiers_defined}
end
@ -1138,8 +1158,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
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, {: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)
@ -1157,13 +1177,13 @@ defmodule Livebook.Runtime.EvaluatorTest do
"""
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
assert_receive {:runtime_evaluation_response, :code_1, {: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, {:text, _}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, _, %{}}, metadata()}
end
end
@ -1185,7 +1205,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
Evaluator.initialize_from(evaluator, parent_evaluator, [:code_1])
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [])
assert_receive {:runtime_evaluation_response, :code_2, {:text, ansi_number(1)}, metadata()}
assert_receive {:runtime_evaluation_response, :code_2,
{:terminal_text, ansi_number(1), %{}}, metadata()}
end
end
@ -1224,7 +1246,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
[]
)
assert_receive {:runtime_evaluation_response, :code_1, {:text, "6"}, metadata()}
assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, "6", %{}},
metadata()}
end
test "mixed erlang/elixir bindings", %{evaluator: evaluator} do
@ -1242,7 +1265,8 @@ 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, {:text, ~S"#{x => 1}"}, metadata()}
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

View file

@ -9,6 +9,7 @@ defmodule Livebook.Session.DataTest do
@eval_resp {:ok, [1, 2, 3]}
@smart_cell_definitions [%{kind: "text", name: "Text", requirement_presets: []}]
@stdout {:terminal_text, "Hello!", %{chunk: true}}
@cid "__anonymous__"
defp eval_meta(opts \\ []) do
@ -78,13 +79,13 @@ defmodule Livebook.Session.DataTest do
Notebook.Section.new()
| id: "s1",
cells: [
%{Notebook.Cell.new(:code) | id: "c1", outputs: [{0, {:stdout, "Hello!"}}]}
%{Notebook.Cell.new(:code) | id: "c1", outputs: [{0, @stdout}]}
]
}
]
}
assert %{notebook: %{sections: [%{cells: [%{outputs: [{0, {:stdout, "Hello!"}}]}]}]}} =
assert %{notebook: %{sections: [%{cells: [%{outputs: [{0, @stdout}]}]}]}} =
Data.new(notebook: notebook)
assert %{notebook: %{sections: [%{cells: [%{outputs: []}]}]}} =
@ -1819,14 +1820,14 @@ defmodule Livebook.Session.DataTest do
{:queue_cells_evaluation, @cid, ["c1"]}
])
operation = {:add_cell_evaluation_output, @cid, "c1", {:stdout, "Hello!"}}
operation = {:add_cell_evaluation_output, @cid, "c1", @stdout}
assert {:ok,
%{
notebook: %{
sections: [
%{
cells: [%{outputs: [{1, {:stdout, "Hello!"}}]}]
cells: [%{outputs: [{1, @stdout}]}]
}
]
}
@ -1842,14 +1843,14 @@ defmodule Livebook.Session.DataTest do
evaluate_cells_operations(["setup", "c1"])
])
operation = {:add_cell_evaluation_output, @cid, "c1", {:stdout, "Hello!"}}
operation = {:add_cell_evaluation_output, @cid, "c1", @stdout}
assert {:ok,
%{
notebook: %{
sections: [
%{
cells: [%{outputs: [{2, {:stdout, "Hello!"}}, _result]}]
cells: [%{outputs: [{2, @stdout}, _result]}]
}
]
}
@ -1868,7 +1869,7 @@ defmodule Livebook.Session.DataTest do
{:notebook_saved, @cid, []}
])
operation = {:add_cell_evaluation_output, @cid, "c1", {:stdout, "Hello!"}}
operation = {:add_cell_evaluation_output, @cid, "c1", @stdout}
assert {:ok, %{dirty: true}, []} = Data.apply_operation(data, operation)
end

View file

@ -187,7 +187,7 @@ defmodule Livebook.SessionTest do
| kind: "text",
source: "chunk 1\n\nchunk 2",
chunks: [{0, 7}, {9, 7}],
outputs: [{1, {:text, "Hello"}}]
outputs: [{1, {:terminal_text, "Hello", %{chunk: false}}}]
}
section = %{Notebook.Section.new() | cells: [smart_cell]}
@ -210,7 +210,7 @@ defmodule Livebook.SessionTest do
assert_receive {:operation,
{:insert_cell, _client_id, ^section_id, 1, :code, _id,
%{source: "chunk 2", outputs: [{1, {:text, "Hello"}}]}}}
%{source: "chunk 2", outputs: [{1, {:terminal_text, "Hello", %{}}}]}}}
end
test "doesn't garbage collect input values" do
@ -920,8 +920,8 @@ defmodule Livebook.SessionTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {: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 +944,8 @@ defmodule Livebook.SessionTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {: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

View file

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

View file

@ -221,7 +221,7 @@ defmodule LivebookWeb.SessionControllerTest do
| source: """
IO.puts("hey")\
""",
outputs: [{0, {:stdout, "hey"}}]
outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}]
}
]
}

View file

@ -81,11 +81,11 @@ defmodule LivebookWeb.AppSessionLiveTest do
| cells: [
%{
Livebook.Notebook.Cell.new(:code)
| source: source_for_output({:stdout, "Printed output"})
| source: source_for_output({:terminal_text, "Printed output", %{chunk: false}})
},
%{
Livebook.Notebook.Cell.new(:code)
| source: source_for_output({:plain_text, "Custom text"})
| source: source_for_output({:plain_text, "Custom text", %{chunk: false}})
}
]
}
@ -121,7 +121,7 @@ defmodule LivebookWeb.AppSessionLiveTest do
| cells: [
%{
Livebook.Notebook.Cell.new(:code)
| source: source_for_output({:stdout, "Printed output"})
| source: source_for_output({:terminal_text, "Printed output", %{chunk: false}})
},
%{
Livebook.Notebook.Cell.new(:code)

View file

@ -176,8 +176,8 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, "\e[32m\"true\"\e[0m"},
_}}
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[32m\"true\"\e[0m", %{chunk: false}}, _}}
end
test "cancelling cell evaluation", %{conn: conn, session: session} do
@ -627,7 +627,7 @@ defmodule LivebookWeb.SessionLiveTest do
end
describe "outputs" do
test "stdout output update", %{conn: conn, session: session} do
test "chunked text output update", %{conn: conn, session: session} do
Session.subscribe(session.id)
evaluate_setup(session.pid)
@ -636,14 +636,21 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
send(session.pid, {:runtime_evaluation_output, cell_id, {:stdout, "line 1\n"}})
send(
session.pid,
{:runtime_evaluation_output, cell_id, {:terminal_text, "line 1\n", %{chunk: true}}}
)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
assert render(view) =~ "line 1"
send(session.pid, {:runtime_evaluation_output, cell_id, {:stdout, "line 2\n"}})
send(
session.pid,
{:runtime_evaluation_output, cell_id, {:terminal_text, "line 2\n", %{chunk: true}}}
)
wait_for_session_update(session.pid)
# Render once, so that stdout send_update is processed
# Render once, so that the send_update is processed
_ = render(view)
assert render(view) =~ "line 2"
end
@ -660,7 +667,7 @@ defmodule LivebookWeb.SessionLiveTest do
send(
session.pid,
{:runtime_evaluation_output, cell_id,
{:frame, [{:text, "In frame"}], %{ref: "1", type: :default}}}
{:frame, [{:terminal_text, "In frame", %{chunk: false}}], %{ref: "1", type: :default}}}
)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
@ -669,7 +676,8 @@ defmodule LivebookWeb.SessionLiveTest do
send(
session.pid,
{:runtime_evaluation_output, cell_id,
{:frame, [{:text, "Updated frame"}], %{ref: "1", type: :replace}}}
{:frame, [{:terminal_text, "Updated frame", %{chunk: false}}],
%{ref: "1", type: :replace}}}
)
wait_for_session_update(session.pid)
@ -696,11 +704,13 @@ defmodule LivebookWeb.SessionLiveTest do
send(
session.pid,
{:runtime_evaluation_output_to, client_id, cell_id, {:stdout, "line 1\n"}}
{:runtime_evaluation_output_to, client_id, cell_id,
{:terminal_text, "line 1\n", %{chunk: true}}}
)
assert_receive {:operation,
{:add_cell_evaluation_output, _, ^cell_id, {:stdout, "line 1\n"}}}
{:add_cell_evaluation_output, _, ^cell_id,
{:terminal_text, "line 1\n", %{chunk: true}}}}
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
refute render(view) =~ "line 1"
@ -721,11 +731,13 @@ defmodule LivebookWeb.SessionLiveTest do
send(
session.pid,
{:runtime_evaluation_output_to_clients, cell_id, {:stdout, "line 1\n"}}
{:runtime_evaluation_output_to_clients, cell_id,
{:terminal_text, "line 1\n", %{chunk: false}}}
)
assert_receive {:operation,
{:add_cell_evaluation_output, _, ^cell_id, {:stdout, "line 1\n"}}}
{:add_cell_evaluation_output, _, ^cell_id,
{:terminal_text, "line 1\n", %{chunk: false}}}}
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
refute render(view) =~ "line 1"
@ -1467,7 +1479,8 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
@ -1514,7 +1527,8 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
@ -1605,7 +1619,8 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, "\e[35mnil\e[0m"}, _}}
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[35mnil\e[0m", %{chunk: false}}, _}}
attrs = params_for(:env_var, name: "MY_AWESOME_ENV", value: "MyEnvVarValue")
Settings.set_env_var(attrs)
@ -1616,7 +1631,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:text, "\e[32m\"MyEnvVarValue\"\e[0m"}, _}}
{:terminal_text, "\e[32m\"MyEnvVarValue\"\e[0m", %{chunk: false}}, _}}
Settings.set_env_var(%{attrs | value: "OTHER_VALUE"})
@ -1626,7 +1641,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id,
{:text, "\e[32m\"OTHER_VALUE\"\e[0m"}, _}}
{:terminal_text, "\e[32m\"OTHER_VALUE\"\e[0m", %{chunk: false}}, _}}
Settings.unset_env_var("MY_AWESOME_ENV")
@ -1635,7 +1650,8 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, "\e[35mnil\e[0m"}, _}}
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, "\e[35mnil\e[0m", %{chunk: false}}, _}}
end
@tag :tmp_dir
@ -1669,7 +1685,8 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
assert output == "\e[32m\"#{String.replace(expected_path, "\\", "\\\\")}\"\e[0m"
@ -1680,7 +1697,8 @@ defmodule LivebookWeb.SessionLiveTest do
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
{:add_cell_evaluation_response, _, ^cell_id,
{:terminal_text, output, %{chunk: false}}, _}}
assert output == "\e[32m\"#{String.replace(initial_os_path, "\\", "\\\\")}\"\e[0m"
end
@ -2009,7 +2027,12 @@ defmodule LivebookWeb.SessionLiveTest do
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
section_id = insert_section(session.pid)
insert_cell_with_output(session.pid, section_id, {:text, "Hello from the app!"})
insert_cell_with_output(
session.pid,
section_id,
{:terminal_text, "Hello from the app!", %{chunk: false}}
)
slug = Livebook.Utils.random_short_id()