mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-11-10 09:03:02 +08:00
Release v0.8.0
This commit is contained in:
parent
2c684dca76
commit
361455cd4e
19 changed files with 350 additions and 311 deletions
2
.github/scripts/app/bootstrap_mac.sh
vendored
2
.github/scripts/app/bootstrap_mac.sh
vendored
|
@ -3,7 +3,7 @@ set -e pipefail
|
|||
|
||||
main() {
|
||||
export MAKEFLAGS=-j$(getconf _NPROCESSORS_ONLN)
|
||||
elixir_vsn="${elixir_vsn:-1.14.0}"
|
||||
elixir_vsn="${elixir_vsn:-1.14.2}"
|
||||
|
||||
mkdir -p tmp/cache
|
||||
. .github/scripts/app/bootstrap_otp_mac.sh
|
||||
|
|
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -7,7 +7,7 @@ on:
|
|||
- "v*.*.*"
|
||||
env:
|
||||
otp: "25.0"
|
||||
elixir: "1.14.0"
|
||||
elixir: "1.14.2"
|
||||
jobs:
|
||||
assets:
|
||||
outputs:
|
||||
|
|
2
.github/workflows/uffizzi-build.yml
vendored
2
.github/workflows/uffizzi-build.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
|||
|
||||
env:
|
||||
otp: "25.0"
|
||||
elixir: "1.14.0"
|
||||
elixir: "1.14.2"
|
||||
|
||||
jobs:
|
||||
build-application:
|
||||
|
|
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [v0.8.0](https://github.com/livebook-dev/livebook/tree/v0.8.0) (2022-12-08)
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced a mechanism for tracking how cells depend on each other and reevaluate only if necessary ([#1517](https://github.com/livebook-dev/livebook/pull/1517))
|
||||
- Improved reproducibility of module definitions ([#1518](https://github.com/livebook-dev/livebook/pull/1518))
|
||||
- Started persisting modules bytecode to disk ([#1521](https://github.com/livebook-dev/livebook/pull/1521))
|
||||
- Support for doctests, now running automatically ([#1525](https://github.com/livebook-dev/livebook/pull/1525))
|
||||
- Support for image input ([#1538](https://github.com/livebook-dev/livebook/pull/1538))
|
||||
- Environment variable for setting app base path, useful when deploying behind a proxy ([#1549](https://github.com/livebook-dev/livebook/pull/1549))
|
||||
- Rendering math in on-hover documentation ([#1566](https://github.com/livebook-dev/livebook/pull/1566))
|
||||
- Support for monospace font in textarea input ([#1565](https://github.com/livebook-dev/livebook/pull/1565))
|
||||
- Added "Neural Network task" and "Slack message" to predefined Smart cells
|
||||
|
||||
### Changed
|
||||
|
||||
- Made the textarea input resize automatically to fit the content ([#1552](https://github.com/livebook-dev/livebook/pull/1552))
|
||||
|
||||
## [v0.7.2](https://github.com/livebook-dev/livebook/tree/v0.7.2) (2022-10-26)
|
||||
|
||||
### Added
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Stage 1
|
||||
# Builds the Livebook release
|
||||
FROM hexpm/elixir:1.14.0-erlang-24.3.4.2-debian-bullseye-20210902-slim AS build
|
||||
FROM hexpm/elixir:1.14.2-erlang-24.3.4.2-debian-bullseye-20210902-slim AS build
|
||||
|
||||
RUN apt-get update && apt-get upgrade -y && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
|
@ -39,7 +39,7 @@ RUN mix do compile, release livebook
|
|||
# We use the same base image, because we need Erlang, Elixir and Mix
|
||||
# during runtime to spawn the Livebook standalone runtimes.
|
||||
# Consequently the release doesn't include ERTS as we have it anyway.
|
||||
FROM hexpm/elixir:1.14.0-erlang-24.3.4.2-debian-bullseye-20210902-slim
|
||||
FROM hexpm/elixir:1.14.2-erlang-24.3.4.2-debian-bullseye-20210902-slim
|
||||
|
||||
RUN apt-get update && apt-get upgrade -y && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.7.0"}
|
||||
{:kino, "~> 0.8.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
@ -159,6 +159,17 @@ above to see them.
|
|||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Kino.Tree
|
||||
|
||||
By default cell results are inspected with a limit on the text size. Inspecting large data structures with no limit makes the representation impractical to read, that's where `Kino.Tree` comes in!
|
||||
|
||||
```elixir
|
||||
data = Process.info(self())
|
||||
Kino.Tree.new(data)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Kino.render/1
|
||||
|
||||
As we saw, Livebook automatically recognises widgets returned
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:maplibre, "~> 0.1.2"},
|
||||
{:kino_maplibre, "~> 0.1.3"}
|
||||
{:maplibre, "~> 0.1.3"},
|
||||
{:kino_maplibre, "~> 0.1.7"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:vega_lite, "~> 0.1.4"},
|
||||
{:kino_vega_lite, "~> 0.1.1"}
|
||||
{:vega_lite, "~> 0.1.6"},
|
||||
{:kino_vega_lite, "~> 0.1.7"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.7.0"}
|
||||
{:kino, "~> 0.8.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.7.0"}
|
||||
{:kino, "~> 0.8.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.7.0"}
|
||||
{:kino, "~> 0.8.0"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.6.1"},
|
||||
{:jason, "~> 1.3"}
|
||||
{:kino, "~> 0.8.0"},
|
||||
{:jason, "~> 1.4"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:kino, "~> 0.7.0"},
|
||||
{:kino_vega_lite, "~> 0.1.4"}
|
||||
{:kino, "~> 0.8.0"},
|
||||
{:kino_vega_lite, "~> 0.1.7"}
|
||||
])
|
||||
```
|
||||
|
||||
|
|
|
@ -19,17 +19,32 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
|||
|
||||
kino_vega_lite = %{
|
||||
name: "kino_vega_lite",
|
||||
dependency: %{dep: {:kino_vega_lite, "~> 0.1.4"}, config: []}
|
||||
dependency: %{dep: {:kino_vega_lite, "~> 0.1.7"}, config: []}
|
||||
}
|
||||
|
||||
kino_db = %{
|
||||
name: "kino_db",
|
||||
dependency: %{dep: {:kino_db, "~> 0.2.0"}, config: []}
|
||||
dependency: %{dep: {:kino_db, "~> 0.2.1"}, config: []}
|
||||
}
|
||||
|
||||
kino_maplibre = %{
|
||||
name: "kino_maplibre",
|
||||
dependency: %{dep: {:kino_maplibre, "~> 0.1.3"}, config: []}
|
||||
dependency: %{dep: {:kino_maplibre, "~> 0.1.7"}, config: []}
|
||||
}
|
||||
|
||||
kino_slack = %{
|
||||
name: "kino_slack",
|
||||
dependency: %{dep: {:kino_slack, "~> 0.1.0"}, config: []}
|
||||
}
|
||||
|
||||
kino_bumblebee = %{
|
||||
name: "kino_bumblebee",
|
||||
dependency: %{dep: {:kino_bumblebee, "~> 0.1.0"}, config: []}
|
||||
}
|
||||
|
||||
exla = %{
|
||||
name: "exla",
|
||||
dependency: %{dep: {:exla, "~> 0.4.1"}, config: [nx: [default_backend: EXLA.Backend]]}
|
||||
}
|
||||
|
||||
@extra_smart_cell_definitions [
|
||||
|
@ -117,6 +132,30 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
%{
|
||||
kind: "Elixir.KinoSlack.MessageCell",
|
||||
name: "Slack message",
|
||||
requirement: %{
|
||||
variants: [
|
||||
%{
|
||||
name: "Default",
|
||||
packages: [kino_slack]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
%{
|
||||
kind: "Elixir.KinoBumblebee.TaskCell",
|
||||
name: "Neural Network task",
|
||||
requirement: %{
|
||||
variants: [
|
||||
%{
|
||||
name: "Default",
|
||||
packages: [kino_bumblebee, exla]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -569,15 +569,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
defp eval(code, binding, env) do
|
||||
try do
|
||||
quoted = Code.string_to_quoted!(code, file: env.file)
|
||||
|
||||
# TODO: remove the else branch when we require Elixir v1.14.2
|
||||
{value, binding, env} =
|
||||
if function_exported?(Code, :eval_quoted_with_env, 4) do
|
||||
Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
|
||||
else
|
||||
Code.eval_quoted_with_env(quoted, binding, env)
|
||||
end
|
||||
|
||||
{value, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
|
||||
{:ok, value, binding, env}
|
||||
catch
|
||||
kind, error ->
|
||||
|
|
|
@ -1,310 +1,303 @@
|
|||
# TODO: remove the else branch when we require Elixir v1.14.2
|
||||
if Version.compare(System.version(), "1.14.2") != :lt do
|
||||
defmodule Livebook.Runtime.Evaluator.Doctests do
|
||||
@moduledoc false
|
||||
defmodule Livebook.Runtime.Evaluator.Doctests do
|
||||
@moduledoc false
|
||||
|
||||
@test_timeout 5_000
|
||||
@line_width 80
|
||||
@pad_size 3
|
||||
@test_timeout 5_000
|
||||
@line_width 80
|
||||
@pad_size 3
|
||||
|
||||
@doc """
|
||||
Runs doctests in the given modules.
|
||||
"""
|
||||
@spec run(list(module())) :: :ok
|
||||
def run(modules)
|
||||
@doc """
|
||||
Runs doctests in the given modules.
|
||||
"""
|
||||
@spec run(list(module())) :: :ok
|
||||
def run(modules)
|
||||
|
||||
def run([]), do: :ok
|
||||
def run([]), do: :ok
|
||||
|
||||
def run(modules) do
|
||||
case define_test_module(modules) do
|
||||
{:ok, test_module} ->
|
||||
if test_module.tests != [] do
|
||||
tests =
|
||||
test_module.tests
|
||||
|> Enum.sort_by(& &1.tags.doctest_line)
|
||||
|> Enum.map(&run_test/1)
|
||||
def run(modules) do
|
||||
case define_test_module(modules) do
|
||||
{:ok, test_module} ->
|
||||
if test_module.tests != [] do
|
||||
tests =
|
||||
test_module.tests
|
||||
|> Enum.sort_by(& &1.tags.doctest_line)
|
||||
|> Enum.map(&run_test/1)
|
||||
|
||||
formatted = format_results(tests)
|
||||
put_output({:text, formatted})
|
||||
end
|
||||
|
||||
delete_test_module(test_module)
|
||||
|
||||
{:error, kind, error} ->
|
||||
put_output({:error, colorize(:red, Exception.format(kind, error, [])), :other})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp define_test_module(modules) do
|
||||
name = Module.concat([LivebookDoctest | modules])
|
||||
|
||||
try do
|
||||
defmodule name do
|
||||
use ExUnit.Case, register: false
|
||||
|
||||
for module <- modules do
|
||||
doctest module
|
||||
end
|
||||
formatted = format_results(tests)
|
||||
put_output({:text, formatted})
|
||||
end
|
||||
|
||||
{:ok, name.__ex_unit__()}
|
||||
catch
|
||||
kind, error -> {:error, kind, error}
|
||||
delete_test_module(test_module)
|
||||
|
||||
{:error, kind, error} ->
|
||||
put_output({:error, colorize(:red, Exception.format(kind, error, [])), :other})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp define_test_module(modules) do
|
||||
name = Module.concat([LivebookDoctest | modules])
|
||||
|
||||
try do
|
||||
defmodule name do
|
||||
use ExUnit.Case, register: false
|
||||
|
||||
for module <- modules do
|
||||
doctest module
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_test_module(%{name: module}) do
|
||||
:code.delete(module)
|
||||
:code.purge(module)
|
||||
end
|
||||
|
||||
defp run_test(test) do
|
||||
runner_pid = self()
|
||||
|
||||
{test_pid, test_ref} =
|
||||
spawn_monitor(fn ->
|
||||
test = exec_test(test)
|
||||
send(runner_pid, {:test_finished, self(), test})
|
||||
end)
|
||||
|
||||
receive_test_reply(test, test_pid, test_ref)
|
||||
end
|
||||
|
||||
defp receive_test_reply(test, test_pid, test_ref) do
|
||||
receive do
|
||||
{:test_finished, ^test_pid, test} ->
|
||||
Process.demonitor(test_ref, [:flush])
|
||||
test
|
||||
|
||||
{:DOWN, ^test_ref, :process, ^test_pid, error} ->
|
||||
%{test | state: failed({:EXIT, test_pid}, error, [])}
|
||||
after
|
||||
@test_timeout ->
|
||||
case Process.info(test_pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
Process.exit(test_pid, :kill)
|
||||
|
||||
receive do
|
||||
{:DOWN, ^test_ref, :process, ^test_pid, _} -> :ok
|
||||
end
|
||||
|
||||
exception = RuntimeError.exception("doctest timed out after #{@test_timeout} ms")
|
||||
%{test | state: failed(:error, exception, prune_stacktrace(stacktrace))}
|
||||
|
||||
nil ->
|
||||
receive_test_reply(test, test_pid, test_ref)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp exec_test(%ExUnit.Test{module: module, name: name} = test) do
|
||||
apply(module, name, [:none])
|
||||
test
|
||||
{:ok, name.__ex_unit__()}
|
||||
catch
|
||||
kind, error ->
|
||||
%{test | state: failed(kind, error, prune_stacktrace(__STACKTRACE__))}
|
||||
kind, error -> {:error, kind, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp failed(kind, reason, stack) do
|
||||
{:failed, {kind, Exception.normalize(kind, reason, stack), stack}}
|
||||
end
|
||||
defp delete_test_module(%{name: module}) do
|
||||
:code.delete(module)
|
||||
:code.purge(module)
|
||||
end
|
||||
|
||||
# As soon as we see our runner module, we ignore the rest of the stacktrace
|
||||
def prune_stacktrace([{__MODULE__, _, _, _} | _]), do: []
|
||||
def prune_stacktrace([h | t]), do: [h | prune_stacktrace(t)]
|
||||
def prune_stacktrace([]), do: []
|
||||
defp run_test(test) do
|
||||
runner_pid = self()
|
||||
|
||||
# Formatting
|
||||
{test_pid, test_ref} =
|
||||
spawn_monitor(fn ->
|
||||
test = exec_test(test)
|
||||
send(runner_pid, {:test_finished, self(), test})
|
||||
end)
|
||||
|
||||
defp format_results(tests) do
|
||||
filed_tests = Enum.reject(tests, &(&1.state == nil))
|
||||
receive_test_reply(test, test_pid, test_ref)
|
||||
end
|
||||
|
||||
test_count = length(tests)
|
||||
failure_count = length(filed_tests)
|
||||
defp receive_test_reply(test, test_pid, test_ref) do
|
||||
receive do
|
||||
{:test_finished, ^test_pid, test} ->
|
||||
Process.demonitor(test_ref, [:flush])
|
||||
test
|
||||
|
||||
doctests_pl = pluralize(test_count, "doctest", "doctests")
|
||||
failures_pl = pluralize(failure_count, "failure", "failures")
|
||||
{:DOWN, ^test_ref, :process, ^test_pid, error} ->
|
||||
%{test | state: failed({:EXIT, test_pid}, error, [])}
|
||||
after
|
||||
@test_timeout ->
|
||||
case Process.info(test_pid, :current_stacktrace) do
|
||||
{:current_stacktrace, stacktrace} ->
|
||||
Process.exit(test_pid, :kill)
|
||||
|
||||
headline =
|
||||
colorize(
|
||||
if(failure_count == 0, do: :green, else: :red),
|
||||
"#{test_count} #{doctests_pl}, #{failure_count} #{failures_pl}"
|
||||
)
|
||||
receive do
|
||||
{:DOWN, ^test_ref, :process, ^test_pid, _} -> :ok
|
||||
end
|
||||
|
||||
failures =
|
||||
for {test, idx} <- Enum.with_index(filed_tests) do
|
||||
{:failed, failure} = test.state
|
||||
exception = RuntimeError.exception("doctest timed out after #{@test_timeout} ms")
|
||||
%{test | state: failed(:error, exception, prune_stacktrace(stacktrace))}
|
||||
|
||||
name =
|
||||
test.name
|
||||
|> Atom.to_string()
|
||||
|> String.replace(~r/ \(\d+\)$/, "")
|
||||
|
||||
line = test.tags.doctest_line
|
||||
|
||||
[
|
||||
"\n\n",
|
||||
"#{idx + 1}) #{name} (line #{line})\n",
|
||||
format_failure(failure, test)
|
||||
]
|
||||
nil ->
|
||||
receive_test_reply(test, test_pid, test_ref)
|
||||
end
|
||||
|
||||
IO.iodata_to_binary([headline, failures])
|
||||
end
|
||||
end
|
||||
|
||||
defp format_failure({:error, %ExUnit.AssertionError{} = reason, _stack}, _test) do
|
||||
diff =
|
||||
ExUnit.Formatter.format_assertion_diff(
|
||||
reason,
|
||||
@pad_size + 2,
|
||||
@line_width,
|
||||
&diff_formatter/2
|
||||
)
|
||||
defp exec_test(%ExUnit.Test{module: module, name: name} = test) do
|
||||
apply(module, name, [:none])
|
||||
test
|
||||
catch
|
||||
kind, error ->
|
||||
%{test | state: failed(kind, error, prune_stacktrace(__STACKTRACE__))}
|
||||
end
|
||||
|
||||
expected = diff[:right]
|
||||
got = diff[:left]
|
||||
source = String.trim(reason.doctest)
|
||||
defp failed(kind, reason, stack) do
|
||||
{:failed, {kind, Exception.normalize(kind, reason, stack), stack}}
|
||||
end
|
||||
|
||||
message_io =
|
||||
if_io(reason.message != "Doctest failed", fn ->
|
||||
message =
|
||||
reason.message
|
||||
|> String.replace_prefix("Doctest failed: ", "")
|
||||
|> pad(@pad_size)
|
||||
# As soon as we see our runner module, we ignore the rest of the stacktrace
|
||||
def prune_stacktrace([{__MODULE__, _, _, _} | _]), do: []
|
||||
def prune_stacktrace([h | t]), do: [h | prune_stacktrace(t)]
|
||||
def prune_stacktrace([]), do: []
|
||||
|
||||
[colorize(:red, message), "\n"]
|
||||
end)
|
||||
# Formatting
|
||||
|
||||
source_io = [
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("doctest"),
|
||||
"\n",
|
||||
pad(source, @pad_size + 2)
|
||||
]
|
||||
defp format_results(tests) do
|
||||
filed_tests = Enum.reject(tests, &(&1.state == nil))
|
||||
|
||||
expected_io =
|
||||
if_io(expected, fn ->
|
||||
[
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("expected"),
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size + 2),
|
||||
expected
|
||||
]
|
||||
end)
|
||||
test_count = length(tests)
|
||||
failure_count = length(filed_tests)
|
||||
|
||||
got_io =
|
||||
if_io(got, fn ->
|
||||
[
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("got"),
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size + 2),
|
||||
got
|
||||
]
|
||||
end)
|
||||
doctests_pl = pluralize(test_count, "doctest", "doctests")
|
||||
failures_pl = pluralize(failure_count, "failure", "failures")
|
||||
|
||||
message_io ++ source_io ++ expected_io ++ got_io
|
||||
end
|
||||
headline =
|
||||
colorize(
|
||||
if(failure_count == 0, do: :green, else: :red),
|
||||
"#{test_count} #{doctests_pl}, #{failure_count} #{failures_pl}"
|
||||
)
|
||||
|
||||
defp format_failure({kind, reason, stacktrace}, test) do
|
||||
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
|
||||
failures =
|
||||
for {test, idx} <- Enum.with_index(filed_tests) do
|
||||
{:failed, failure} = test.state
|
||||
|
||||
banner =
|
||||
case blamed do
|
||||
%FunctionClauseError{} ->
|
||||
banner = Exception.format_banner(kind, reason, stacktrace)
|
||||
inspect_opts = Livebook.Runtime.Evaluator.DefaultFormatter.inspect_opts()
|
||||
blame = FunctionClauseError.blame(blamed, &inspect(&1, inspect_opts), &blame_match/1)
|
||||
colorize(:red, banner) <> blame
|
||||
name =
|
||||
test.name
|
||||
|> Atom.to_string()
|
||||
|> String.replace(~r/ \(\d+\)$/, "")
|
||||
|
||||
_ ->
|
||||
banner = Exception.format_banner(kind, blamed, stacktrace)
|
||||
colorize(:red, banner)
|
||||
end
|
||||
line = test.tags.doctest_line
|
||||
|
||||
if stacktrace == [] do
|
||||
pad(banner, @pad_size)
|
||||
else
|
||||
[
|
||||
pad(banner, @pad_size),
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("stacktrace"),
|
||||
format_stacktrace(stacktrace, test.module, test.name)
|
||||
"\n\n",
|
||||
"#{idx + 1}) #{name} (line #{line})\n",
|
||||
format_failure(failure, test)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp if_io(value, fun), do: if(value, do: fun.(), else: [])
|
||||
IO.iodata_to_binary([headline, failures])
|
||||
end
|
||||
|
||||
defp format_stacktrace(stacktrace, test_case, test) do
|
||||
for entry <- stacktrace do
|
||||
message = format_stacktrace_entry(entry, test_case, test)
|
||||
"\n" <> pad(message, @pad_size + 2)
|
||||
defp format_failure({:error, %ExUnit.AssertionError{} = reason, _stack}, _test) do
|
||||
diff =
|
||||
ExUnit.Formatter.format_assertion_diff(
|
||||
reason,
|
||||
@pad_size + 2,
|
||||
@line_width,
|
||||
&diff_formatter/2
|
||||
)
|
||||
|
||||
expected = diff[:right]
|
||||
got = diff[:left]
|
||||
source = String.trim(reason.doctest)
|
||||
|
||||
message_io =
|
||||
if_io(reason.message != "Doctest failed", fn ->
|
||||
message =
|
||||
reason.message
|
||||
|> String.replace_prefix("Doctest failed: ", "")
|
||||
|> pad(@pad_size)
|
||||
|
||||
[colorize(:red, message), "\n"]
|
||||
end)
|
||||
|
||||
source_io = [
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("doctest"),
|
||||
"\n",
|
||||
pad(source, @pad_size + 2)
|
||||
]
|
||||
|
||||
expected_io =
|
||||
if_io(expected, fn ->
|
||||
[
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("expected"),
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size + 2),
|
||||
expected
|
||||
]
|
||||
end)
|
||||
|
||||
got_io =
|
||||
if_io(got, fn ->
|
||||
[
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("got"),
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size + 2),
|
||||
got
|
||||
]
|
||||
end)
|
||||
|
||||
message_io ++ source_io ++ expected_io ++ got_io
|
||||
end
|
||||
|
||||
defp format_failure({kind, reason, stacktrace}, test) do
|
||||
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
|
||||
|
||||
banner =
|
||||
case blamed do
|
||||
%FunctionClauseError{} ->
|
||||
banner = Exception.format_banner(kind, reason, stacktrace)
|
||||
inspect_opts = Livebook.Runtime.Evaluator.DefaultFormatter.inspect_opts()
|
||||
blame = FunctionClauseError.blame(blamed, &inspect(&1, inspect_opts), &blame_match/1)
|
||||
colorize(:red, banner) <> blame
|
||||
|
||||
_ ->
|
||||
banner = Exception.format_banner(kind, blamed, stacktrace)
|
||||
colorize(:red, banner)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
|
||||
"doctest at line #{location[:line]}"
|
||||
end
|
||||
|
||||
defp format_stacktrace_entry(entry, _test_case, _test) do
|
||||
Exception.format_stacktrace_entry(entry)
|
||||
end
|
||||
|
||||
defp format_label(label), do: colorize(:cyan, "#{label}:")
|
||||
|
||||
defp pad(string, pad_size) do
|
||||
padding = String.duplicate(" ", pad_size)
|
||||
padding <> String.replace(string, "\n", "\n" <> padding)
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}), do: Macro.to_string(node)
|
||||
|
||||
defp blame_match(%{match?: false, node: node}) do
|
||||
colorize(:red, Macro.to_string(node))
|
||||
end
|
||||
|
||||
defp diff_formatter(:diff_enabled?, _doc), do: true
|
||||
|
||||
defp diff_formatter(key, doc) do
|
||||
colors = [
|
||||
diff_delete: :red,
|
||||
diff_delete_whitespace: IO.ANSI.color_background(4, 0, 0),
|
||||
diff_insert: :green,
|
||||
diff_insert_whitespace: IO.ANSI.color_background(0, 3, 0)
|
||||
if stacktrace == [] do
|
||||
pad(banner, @pad_size)
|
||||
else
|
||||
[
|
||||
pad(banner, @pad_size),
|
||||
"\n",
|
||||
String.duplicate(" ", @pad_size),
|
||||
format_label("stacktrace"),
|
||||
format_stacktrace(stacktrace, test.module, test.name)
|
||||
]
|
||||
|
||||
Inspect.Algebra.color(doc, key, %Inspect.Opts{syntax_colors: colors})
|
||||
end
|
||||
|
||||
defp colorize(color, string) do
|
||||
[color, string, :reset]
|
||||
|> IO.ANSI.format_fragment(true)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp pluralize(1, singular, _plural), do: singular
|
||||
defp pluralize(_, _singular, plural), do: plural
|
||||
|
||||
defp put_output(output) do
|
||||
gl = Process.group_leader()
|
||||
ref = make_ref()
|
||||
|
||||
send(gl, {:io_request, self(), ref, {:livebook_put_output, output}})
|
||||
|
||||
receive do
|
||||
{:io_reply, ^ref, reply} -> {:ok, reply}
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
defmodule Livebook.Runtime.Evaluator.Doctests do
|
||||
def run(_modules), do: :ok
|
||||
|
||||
defp if_io(value, fun), do: if(value, do: fun.(), else: [])
|
||||
|
||||
defp format_stacktrace(stacktrace, test_case, test) do
|
||||
for entry <- stacktrace do
|
||||
message = format_stacktrace_entry(entry, test_case, test)
|
||||
"\n" <> pad(message, @pad_size + 2)
|
||||
end
|
||||
end
|
||||
|
||||
defp format_stacktrace_entry({test_case, test, _, location}, test_case, test) do
|
||||
"doctest at line #{location[:line]}"
|
||||
end
|
||||
|
||||
defp format_stacktrace_entry(entry, _test_case, _test) do
|
||||
Exception.format_stacktrace_entry(entry)
|
||||
end
|
||||
|
||||
defp format_label(label), do: colorize(:cyan, "#{label}:")
|
||||
|
||||
defp pad(string, pad_size) do
|
||||
padding = String.duplicate(" ", pad_size)
|
||||
padding <> String.replace(string, "\n", "\n" <> padding)
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}), do: Macro.to_string(node)
|
||||
|
||||
defp blame_match(%{match?: false, node: node}) do
|
||||
colorize(:red, Macro.to_string(node))
|
||||
end
|
||||
|
||||
defp diff_formatter(:diff_enabled?, _doc), do: true
|
||||
|
||||
defp diff_formatter(key, doc) do
|
||||
colors = [
|
||||
diff_delete: :red,
|
||||
diff_delete_whitespace: IO.ANSI.color_background(4, 0, 0),
|
||||
diff_insert: :green,
|
||||
diff_insert_whitespace: IO.ANSI.color_background(0, 3, 0)
|
||||
]
|
||||
|
||||
Inspect.Algebra.color(doc, key, %Inspect.Opts{syntax_colors: colors})
|
||||
end
|
||||
|
||||
defp colorize(color, string) do
|
||||
[color, string, :reset]
|
||||
|> IO.ANSI.format_fragment(true)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp pluralize(1, singular, _plural), do: singular
|
||||
defp pluralize(_, _singular, plural), do: plural
|
||||
|
||||
defp put_output(output) do
|
||||
gl = Process.group_leader()
|
||||
ref = make_ref()
|
||||
|
||||
send(gl, {:io_request, self(), ref, {:livebook_put_output, output}})
|
||||
|
||||
receive do
|
||||
{:io_reply, ^ref, reply} -> {:ok, reply}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -272,20 +272,6 @@ defmodule LivebookWeb.Output do
|
|||
render_formatted_error_message(formatted)
|
||||
end
|
||||
|
||||
# TODO: remove on Livebook v0.7
|
||||
defp render_output(output, %{})
|
||||
when elem(output, 0) in [
|
||||
:vega_lite_static,
|
||||
:vega_lite_dynamic,
|
||||
:table_dynamic,
|
||||
:frame_dynamic
|
||||
] do
|
||||
render_error_message("""
|
||||
Legacy output format: #{inspect(output)}. Please update Kino to
|
||||
the latest version.
|
||||
""")
|
||||
end
|
||||
|
||||
defp render_output(output, %{}) do
|
||||
render_error_message("""
|
||||
Unknown output format: #{inspect(output)}. If you're using Kino,
|
||||
|
|
|
@ -2054,7 +2054,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:apply_cell_delta, _client_id, _cell_id, _tag, _delta, _revision} ->
|
||||
update_dirty_status(data_view, data)
|
||||
|
||||
{:update_smart_cell, _client_id, _cell_id, _cell_state, _delta, _reevaluate} ->
|
||||
{:update_smart_cell, _client_id, _cell_id, _cell_state, _delta, _chunks, _reevaluate} ->
|
||||
update_dirty_status(data_view, data)
|
||||
|
||||
# For outputs that update existing outputs we send the update directly
|
||||
|
|
6
mix.exs
6
mix.exs
|
@ -1,11 +1,11 @@
|
|||
defmodule Livebook.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@elixir_requirement "~> 1.14"
|
||||
@version "0.7.2"
|
||||
@elixir_requirement "~> 1.14.2"
|
||||
@version "0.8.0"
|
||||
@description "Interactive and collaborative code notebooks - made with Phoenix LiveView"
|
||||
|
||||
@app_elixir_version "1.14.0"
|
||||
@app_elixir_version "1.14.2"
|
||||
@app_rebar3_version "3.19.0"
|
||||
|
||||
def project do
|
||||
|
|
Loading…
Reference in a new issue