From 3c56b87154bf2fcb7c011dbe5ab665e777f837a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Tue, 30 May 2023 22:03:20 +0200 Subject: [PATCH] Doctest updates (#1944) --- assets/css/ansi.css | 36 ++-- assets/css/editor.css | 38 ++-- assets/js/hooks/cell.js | 16 +- assets/js/hooks/cell_editor/live_editor.js | 124 +++---------- .../hooks/cell_editor/live_editor/doctest.js | 134 ++++++++++++++ lib/livebook/runtime.ex | 25 +++ lib/livebook/runtime/evaluator/doctests.ex | 40 ++-- lib/livebook/runtime/evaluator/io_proxy.ex | 5 + lib/livebook/session.ex | 5 + lib/livebook/session/data.ex | 6 +- lib/livebook_web/live/session_live.ex | 40 ++-- test/livebook/runtime/evaluator_test.exs | 173 ++++++++---------- 12 files changed, 355 insertions(+), 287 deletions(-) create mode 100644 assets/js/hooks/cell_editor/live_editor/doctest.js diff --git a/assets/css/ansi.css b/assets/css/ansi.css index 2e3bbd0db..6fed2c692 100644 --- a/assets/css/ansi.css +++ b/assets/css/ansi.css @@ -1,8 +1,8 @@ /* Variables for HTML-ized ANSI string. -Many colors are taken from the One Light theme -to be consistent with the editor. +Many colors are taken from the One Light and One Dark theme for +consistency with the editor. */ :root { @@ -24,19 +24,21 @@ to be consistent with the editor. --ansi-color-light-white: white; } -/* The same as above but brightned by 10% */ -[data-editor-theme="default"] { - --ansi-color-red: #dd1f53; - --ansi-color-green: #5ab756; - --ansi-color-yellow: #d9930b; - --ansi-color-blue: #4d8cfb; - --ansi-color-magenta: #b02fbb; - --ansi-color-cyan: #05a4d0; - --ansi-color-light-black: #676e7b; - --ansi-color-light-red: #f35c57; - --ansi-color-light-green: #42dcab; - --ansi-color-light-yellow: #fdea9a; - --ansi-color-light-blue: #77c0fc; - --ansi-color-light-magenta: #d181e5; - --ansi-color-light-cyan: #64ccda; +body[data-editor-theme="default"] .editor-theme-aware-ansi { + --ansi-color-black: black; + --ansi-color-red: #be5046; + --ansi-color-green: #98c379; + --ansi-color-yellow: #e5c07b; + --ansi-color-blue: #61afef; + --ansi-color-magenta: #c678dd; + --ansi-color-cyan: #56b6c2; + --ansi-color-white: white; + --ansi-color-light-black: #5c6370; + --ansi-color-light-red: #e06c75; + --ansi-color-light-green: #34d399; + --ansi-color-light-yellow: #fde68a; + --ansi-color-light-blue: #93c5fd; + --ansi-color-light-magenta: #f472b6; + --ansi-color-light-cyan: #6be3f2; + --ansi-color-light-white: white; } diff --git a/assets/css/editor.css b/assets/css/editor.css index 485324d37..121b9d28b 100644 --- a/assets/css/editor.css +++ b/assets/css/editor.css @@ -159,46 +159,50 @@ Also some spacing adjustments. transform: none; } -/* To style circles for doctest results */ -.line-circle-red, -.line-circle-green, -.line-circle-grey { +/* === Doctest status decoration === */ + +.doctest-status-decoration-running, +.doctest-status-decoration-success, +.doctest-status-decoration-failed { height: 100%; position: relative; } -.line-circle-red::after, -.line-circle-green::after, -.line-circle-grey::after { +.doctest-status-decoration-running::after, +.doctest-status-decoration-success::after, +.doctest-status-decoration-failed::after { box-sizing: border-box; border-radius: 2px; content: ""; display: block; height: 12px; width: 12px; - margin-left: 6px; position: absolute; top: 50%; - transform: translateY(-50%); + left: 50%; + transform: translate(-50%, -50%); } -.line-circle-red::after { - background-color: rgb(233 117 121); +.doctest-status-decoration-running::after { + @apply bg-gray-400; } -.line-circle-green::after { - background-color: rgb(74 222 128); +.doctest-status-decoration-success::after { + @apply bg-green-bright-400; } -.line-circle-grey::after { - background-color: rgb(97 117 138); +.doctest-status-decoration-failed::after { + @apply bg-red-400; } -.doctest-failure-overlay { +/* === Doctest failure details === */ + +.doctest-details-widget { @apply font-editor; white-space: pre; background-color: rgba(0, 0, 0, 0.05); - padding-left: calc(68px + 6ch); + padding-top: 6px; + padding-bottom: 6px; position: absolute; width: 100%; } diff --git a/assets/js/hooks/cell.js b/assets/js/hooks/cell.js index dfdb7fd66..678574cea 100644 --- a/assets/js/hooks/cell.js +++ b/assets/js/hooks/cell.js @@ -248,19 +248,9 @@ const Cell = { }); this.handleEvent( - `doctest_result:${this.props.cellId}`, - ({ state, column, line, end_line, contents }) => { - switch (state) { - case "evaluating": - liveEditor.addEvaluatingDoctest(line); - break; - case "success": - liveEditor.addSuccessDoctest(line); - break; - case "failed": - liveEditor.addFailedDoctest(column, line, end_line, contents); - break; - } + `doctest_report:${this.props.cellId}`, + (doctestReport) => { + liveEditor.updateDoctest(doctestReport); } ); } diff --git a/assets/js/hooks/cell_editor/live_editor.js b/assets/js/hooks/cell_editor/live_editor.js index 964a9bc93..84c5ec009 100644 --- a/assets/js/hooks/cell_editor/live_editor.js +++ b/assets/js/hooks/cell_editor/live_editor.js @@ -7,6 +7,7 @@ import HookServerAdapter from "./live_editor/hook_server_adapter"; import RemoteUser from "./live_editor/remote_user"; import { replacedSuffixLength } from "../../lib/text_utils"; import { settingsStore, EDITOR_FONT_SIZE } from "../../lib/settings"; +import Doctest from "./live_editor/doctest"; /** * Mounts cell source editor with real-time collaboration mechanism. @@ -35,19 +36,7 @@ class LiveEditor { this._onBlur = []; this._onCursorSelectionChange = []; this._remoteUserByClientId = {}; - - /* For doctest decorations we store the params to create the - * decorations and also the result of creating the decorations. - * The params are IModelDeltaDecoration from https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IModelDeltaDecoration.html - * and the result is IEditorDecorationsCollection from https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorDecorationsCollection.html - */ - this._doctestDecorations = { - deltaDecorations: {}, - decorationCollection: null, - }; - - this._doctestZones = []; - this._doctestOverlays = []; + this._doctestByLine = {}; const serverAdapter = new HookServerAdapter(hook, cellId, tag); this.editorClient = new EditorClient(serverAdapter, revision); @@ -209,6 +198,33 @@ class LiveEditor { } } + /** + * Either adds or updates doctest indicators. + */ + updateDoctest(doctestReport) { + this._ensureMounted(); + + if (this._doctestByLine[doctestReport.line]) { + this._doctestByLine[doctestReport.line].update(doctestReport); + } else { + this._doctestByLine[doctestReport.line] = new Doctest( + this.editor, + doctestReport + ); + } + } + + /** + * Removes doctest indicators. + */ + clearDoctests() { + this._ensureMounted(); + + Object.values(this._doctestByLine).forEach((doctest) => doctest.dispose()); + + this._doctestByLine = {}; + } + /** * Sets underline markers for warnings and errors. * @@ -243,8 +259,6 @@ class LiveEditor { _mountEditor() { const settings = settingsStore.get(); - this.settings = settings; - this.editor = monaco.editor.create(this.container, { language: this.language, value: this.source, @@ -286,9 +300,6 @@ class LiveEditor { : "off", }); - this._doctestDecorations.decorationCollection = - this.editor.createDecorationsCollection([]); - this.editor.addAction({ contextMenuGroupId: "word-wrapping", id: "enable-word-wrapping", @@ -585,83 +596,6 @@ class LiveEditor { ); }); } - - clearDoctests() { - this._doctestDecorations.decorationCollection.clear(); - this._doctestDecorations.deltaDecorations = {}; - this._doctestOverlays.forEach((overlay) => - this.editor.removeOverlayWidget(overlay) - ); - this.editor.changeViewZones((changeAccessor) => { - this._doctestZones.forEach((zone) => changeAccessor.removeZone(zone)); - }); - } - - _createDoctestDecoration(lineNumber, className) { - return { - range: new monaco.Range(lineNumber, 1, lineNumber, 1), - options: { - isWholeLine: true, - linesDecorationsClassName: className, - }, - }; - } - - _addDoctestDecoration(line, className) { - const newDecoration = this._createDoctestDecoration(line, className); - this._doctestDecorations.deltaDecorations[line] = newDecoration; - const decos = Object.values(this._doctestDecorations.deltaDecorations); - this._doctestDecorations.decorationCollection.set(decos); - } - - _addDoctestOverlay(column, line, endLine, contents) { - let overlayDom = document.createElement("div"); - overlayDom.innerHTML = contents.join("\n"); - overlayDom.classList.add("doctest-failure-overlay"); - overlayDom.style.fontSize = `${this.settings.editor_font_size}px`; - overlayDom.style.paddingLeft = - this.settings.editor_font_size === EDITOR_FONT_SIZE.large - ? `calc(74px + ${column}ch)` - : `calc(68px + ${column}ch)`; - - // https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ioverlaywidget.html - let overlayWidget = { - getId: () => `doctest-overlay-${line}`, - getDomNode: () => overlayDom, - getPosition: () => null, - }; - this.editor.addOverlayWidget(overlayWidget); - this._doctestOverlays.push(overlayWidget); - - this.editor.changeViewZones((changeAccessor) => { - this._doctestZones.push( - changeAccessor.addZone({ - afterLineNumber: endLine, - heightInLines: contents.length, - domNode: document.createElement("div"), - onDomNodeTop: (top) => { - overlayDom.style.top = top + "px"; - }, - onComputedHeight: (height) => { - overlayDom.style.height = height + "px"; - }, - }) - ); - }); - } - - addSuccessDoctest(line) { - this._addDoctestDecoration(line, "line-circle-green"); - } - - addFailedDoctest(column, line, endLine, contents) { - this._addDoctestDecoration(line, "line-circle-red"); - this._addDoctestOverlay(column, line, endLine, contents); - } - - addEvaluatingDoctest(line) { - this._addDoctestDecoration(line, "line-circle-grey"); - } } function completionItemsToSuggestions(items, settings) { diff --git a/assets/js/hooks/cell_editor/live_editor/doctest.js b/assets/js/hooks/cell_editor/live_editor/doctest.js new file mode 100644 index 000000000..b8ffc6159 --- /dev/null +++ b/assets/js/hooks/cell_editor/live_editor/doctest.js @@ -0,0 +1,134 @@ +import monaco from "./monaco"; + +/** + * Doctest visual indicators within the editor. + * + * Consists of a status widget and optional error details. + */ +export default class Doctest { + constructor(editor, doctestReport) { + this._editor = editor; + + this._statusDecoration = new StatusDecoration( + editor, + doctestReport.line, + doctestReport.status + ); + + if (doctestReport.status === "failed") { + this._detailsWidget = new DetailsWidget(editor, doctestReport); + } + } + + /** + * Updates doctest indicator. + */ + update(doctestReport) { + this._statusDecoration.update(doctestReport.status); + + if (doctestReport.status === "failed") { + this._detailsWidget && this._detailsWidget.dispose(); + this._detailsWidget = new DetailsWidget(this._editor, doctestReport); + } + } + + /** + * Performs necessary cleanup actions. + */ + dispose() { + this._statusDecoration.dispose(); + this._detailsWidget && this._detailsWidget.dispose(); + } +} + +class StatusDecoration { + constructor(editor, lineNumber, status) { + this._editor = editor; + this._lineNumber = lineNumber; + this._decorations = []; + + this.update(status); + } + + update(status) { + const newDecorations = [ + { + range: new monaco.Range(this._lineNumber, 1, this._lineNumber, 1), + options: { + isWholeLine: true, + linesDecorationsClassName: `doctest-status-decoration-${status}`, + }, + }, + ]; + + this._decorations = this._editor.deltaDecorations( + this._decorations, + newDecorations + ); + } + + dispose() { + this._editor.deltaDecorations(this._decorations, []); + } +} + +class DetailsWidget { + constructor(editor, doctestReport) { + this._editor = editor; + + const { line, end_line, details, column } = doctestReport; + const detailsHtml = details.join("\n"); + const numberOfLines = details.length; + + const marginWidth = this._editor + .getDomNode() + .querySelector(".margin-view-overlays").offsetWidth; + + const fontSize = this._editor.getOption( + monaco.editor.EditorOption.fontSize + ); + + const lineHeight = this._editor.getOption( + monaco.editor.EditorOption.lineHeight + ); + + const detailsNode = document.createElement("div"); + detailsNode.innerHTML = detailsHtml; + detailsNode.classList.add( + "doctest-details-widget", + "editor-theme-aware-ansi" + ); + detailsNode.style.fontSize = `${fontSize}px`; + detailsNode.style.paddingLeft = `calc(${marginWidth}px + ${column}ch)`; + + this._overlayWidget = { + getId: () => `livebook.doctest.overlay.${line}`, + getDomNode: () => detailsNode, + getPosition: () => null, + }; + + this._editor.addOverlayWidget(this._overlayWidget); + + this._editor.changeViewZones((changeAccessor) => { + this._viewZone = changeAccessor.addZone({ + afterLineNumber: end_line, + // Placeholder for all lines and additional padding + heightInPx: numberOfLines * lineHeight + 12, + domNode: document.createElement("div"), + onDomNodeTop: (top) => { + detailsNode.style.top = `${top}px`; + }, + onComputedHeight: (height) => { + detailsNode.style.height = `${height}px`; + }, + }); + }); + } + + dispose() { + this._editor.removeOverlayWidget(this._overlayWidget); + this._editor.changeViewZones((changeAccessor) => { + changeAccessor.removeZone(this._viewZone); + }); + } +} diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index 7b283455b..6db8a7b1a 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -92,6 +92,24 @@ defprotocol Livebook.Runtime do identifiers_defined: %{(identifier :: term()) => version :: term()} } + @typedoc """ + Includes information about a running or finished doctest. + + Failed doctests have additional details formatted as a string. + """ + @type doctest_report :: + %{ + status: :running | :success, + line: pos_integer() + } + | %{ + status: :failed, + column: pos_integer(), + line: pos_integer(), + end_line: pos_integer(), + details: String.t() + } + @typedoc """ Recognised intellisense request. """ @@ -403,6 +421,13 @@ defprotocol Livebook.Runtime do to notify the owner. + ### Doctests + + If the cell includes doctests, the runtime can evaluate them and + send reports as a message: + + * `{:runtime_doctest_report, evaluation_ref, doctest_report}` + ## Options * `:file` - the file considered as the source during evaluation. diff --git a/lib/livebook/runtime/evaluator/doctests.ex b/lib/livebook/runtime/evaluator/doctests.ex index 8970cc1dd..93b159f64 100644 --- a/lib/livebook/runtime/evaluator/doctests.ex +++ b/lib/livebook/runtime/evaluator/doctests.ex @@ -22,7 +22,7 @@ defmodule Livebook.Runtime.Evaluator.Doctests do test_module.tests |> Enum.sort_by(& &1.tags.doctest_line) |> Enum.each(fn test -> - report_doctest_evaluating(test) + report_doctest_running(test) test = run_test(test) report_doctest_result(test, lines) test @@ -38,22 +38,18 @@ defmodule Livebook.Runtime.Evaluator.Doctests do :ok end - defp report_doctest_evaluating(test) do - result = %{ + defp report_doctest_running(test) do + send_doctest_report(%{ line: test.tags.doctest_line, - state: :evaluating - } - - put_output({:doctest_result, result}) + status: :running + }) end defp report_doctest_result(%{state: nil} = test, _lines) do - result = %{ + send_doctest_report(%{ line: test.tags.doctest_line, - state: :success - } - - put_output({:doctest_result, result}) + status: :success + }) end defp report_doctest_result(%{state: {:failed, failure}} = test, lines) do @@ -92,15 +88,13 @@ defmodule Livebook.Runtime.Evaluator.Doctests do end_line end - result = %{ + send_doctest_report(%{ column: count_columns(prompt_line, 0), line: doctest_line, end_line: end_line, - state: :failed, - contents: IO.iodata_to_binary(format_failure(failure, test)) - } - - put_output({:doctest_result, result}) + status: :failed, + details: IO.iodata_to_binary(format_failure(failure, test)) + }) end defp count_columns(" " <> rest, counter), do: count_columns(rest, counter + 1) @@ -338,10 +332,18 @@ defmodule Livebook.Runtime.Evaluator.Doctests do end defp put_output(output) do + send_livebook_message({:livebook_put_output, output}) + end + + defp send_doctest_report(doctest_report) do + send_livebook_message({:livebook_doctest_report, doctest_report}) + end + + defp send_livebook_message(message) do gl = Process.group_leader() ref = make_ref() - send(gl, {:io_request, self(), ref, {:livebook_put_output, output}}) + send(gl, {:io_request, self(), ref, message}) receive do {:io_reply, ^ref, reply} -> {:ok, reply} diff --git a/lib/livebook/runtime/evaluator/io_proxy.ex b/lib/livebook/runtime/evaluator/io_proxy.ex index 5b6b11f9f..4174549c6 100644 --- a/lib/livebook/runtime/evaluator/io_proxy.ex +++ b/lib/livebook/runtime/evaluator/io_proxy.ex @@ -240,6 +240,11 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do {:ok, state} end + defp io_request({:livebook_doctest_report, doctest_report}, state) do + send(state.send_to, {:runtime_doctest_report, state.ref, doctest_report}) + {:ok, state} + end + defp io_request({:livebook_get_input_value, input_id}, state) do input_cache = Map.put_new_lazy(state.input_cache, input_id, fn -> diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex index 989247b14..d07f992d3 100644 --- a/lib/livebook/session.ex +++ b/lib/livebook/session.ex @@ -1324,6 +1324,11 @@ defmodule Livebook.Session do {:noreply, state} end + def handle_info({:runtime_doctest_report, cell_id, doctest_report}, state) do + operation = {:add_cell_doctest_report, @client_id, cell_id, doctest_report} + {:noreply, handle_operation(state, operation)} + end + def handle_info({:runtime_evaluation_output_to_clients, cell_id, output}, state) do operation = {:add_cell_evaluation_output, @client_id, cell_id, output} broadcast_operation(state.session_id, operation) diff --git a/lib/livebook/session/data.ex b/lib/livebook/session/data.ex index 55951ed72..1601ba295 100644 --- a/lib/livebook/session/data.ex +++ b/lib/livebook/session/data.ex @@ -181,6 +181,7 @@ defmodule Livebook.Session.Data do | {:move_cell, client_id(), Cell.id(), offset :: integer()} | {:move_section, client_id(), Section.id(), offset :: integer()} | {:queue_cells_evaluation, client_id(), list(Cell.id())} + | {:add_cell_doctest_report, client_id(), Cell.id(), Runtime.doctest_report()} | {:add_cell_evaluation_output, client_id(), Cell.id(), term()} | {:add_cell_evaluation_response, client_id(), Cell.id(), term(), metadata :: map()} | {:bind_input, client_id(), code_cell_id :: Cell.id(), input_id()} @@ -546,10 +547,7 @@ defmodule Livebook.Session.Data do end end - def apply_operation( - data, - {:add_cell_evaluation_output, _client_id, id, {:doctest_result, _result}} - ) do + def apply_operation(data, {:add_cell_doctest_report, _client_id, id, _doctest_report}) do with {:ok, _cell, _} <- Notebook.fetch_cell_and_section(data.notebook, id) do data |> with_actions() diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 3df9340bd..9d4fb1f9e 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -1772,25 +1772,6 @@ defmodule LivebookWeb.SessionLive do end end - defp after_operation( - socket, - _prev_socket, - {:add_cell_evaluation_output, _client_id, cell_id, {:doctest_result, result}} - ) do - result = - Map.replace_lazy( - result, - :contents, - fn contents -> - contents - |> LivebookWeb.Helpers.ANSI.ansi_string_to_html_lines() - |> Enum.map(&Phoenix.HTML.safe_to_string/1) - end - ) - - push_event(socket, "doctest_result:#{cell_id}", result) - end - defp after_operation( socket, _prev_socket, @@ -1809,6 +1790,21 @@ defmodule LivebookWeb.SessionLive do |> push_event("evaluation_finished:#{cell_id}", %{code_markers: metadata.code_markers}) end + defp after_operation( + socket, + _prev_socket, + {:add_cell_doctest_report, _client_id, cell_id, doctest_report} + ) do + doctest_report = + Map.replace_lazy(doctest_report, :details, fn details -> + details + |> LivebookWeb.Helpers.ANSI.ansi_string_to_html_lines() + |> Enum.map(&Phoenix.HTML.safe_to_string/1) + end) + + push_event(socket, "doctest_report:#{cell_id}", doctest_report) + end + defp after_operation( socket, _prev_socket, @@ -2264,9 +2260,6 @@ defmodule LivebookWeb.SessionLive do data_view - {:add_cell_evaluation_output, _client_id, _cell_id, {:doctest_result, _result}} -> - data_view - {:add_cell_evaluation_output, _client_id, cell_id, {:stdout, text}} -> # Lookup in previous data to see if the output is already there case Notebook.fetch_cell_and_section(prev_data.notebook, cell_id) do @@ -2278,6 +2271,9 @@ defmodule LivebookWeb.SessionLive do data_to_view(data) end + {:doctest_report, _client_id, _cell_id, _doctest_report} -> + data_view + _ -> data_to_view(data) end diff --git a/test/livebook/runtime/evaluator_test.exs b/test/livebook/runtime/evaluator_test.exs index 9d5e61fd4..54167e2c1 100644 --- a/test/livebook/runtime/evaluator_test.exs +++ b/test/livebook/runtime/evaluator_test.exs @@ -437,49 +437,37 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, code, :code_1, []) - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 4, state: :evaluating}}} + assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}} - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, - %{ - column: 6, - contents: - "\e[31mexpected exception ArgumentError but got RuntimeError with message \"oops\"\e[0m", - end_line: 5, - line: 4, - state: :failed - }}} - - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 7, state: :evaluating}}} - - assert_receive {:runtime_evaluation_output, :code_1, - { - :doctest_result, - %{ - column: 6, - contents: "\e[31mExpected truthy, got false\e[0m", - end_line: 8, - line: 7, - state: :failed - } + assert_receive {:runtime_doctest_report, :code_1, + %{ + column: 6, + details: + "\e[31mexpected exception ArgumentError but got RuntimeError with message \"oops\"\e[0m", + end_line: 5, + line: 4, + status: :failed }} - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 12, state: :evaluating}}} + assert_receive {:runtime_doctest_report, :code_1, %{line: 7, status: :running}} - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 12, state: :success}}} - - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 19, state: :evaluating}}} - - assert_receive {:runtime_evaluation_output, :code_1, - { - :doctest_result, - %{column: 4, contents: _, end_line: 20, line: 19, state: :failed} + assert_receive {:runtime_doctest_report, :code_1, + %{ + column: 6, + details: "\e[31mExpected truthy, got false\e[0m", + end_line: 8, + line: 7, + status: :failed }} + + assert_receive {:runtime_doctest_report, :code_1, %{line: 12, status: :running}} + + assert_receive {:runtime_doctest_report, :code_1, %{line: 12, status: :success}} + + assert_receive {:runtime_doctest_report, :code_1, %{line: 19, status: :running}} + + assert_receive {:runtime_doctest_report, :code_1, + %{column: 4, details: _, end_line: 20, line: 19, status: :failed}} end # TODO: Run this test on Elixir v1.15+ @@ -502,18 +490,16 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, code, :code_1, []) - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 4, state: :evaluating}}} + assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}} - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, - %{ - column: 6, - contents: _, - end_line: 7, - line: 4, - state: :failed - }}} + assert_receive {:runtime_doctest_report, :code_1, + %{ + column: 6, + details: _, + end_line: 7, + line: 4, + status: :failed + }} end test "runtime errors", %{evaluator: evaluator} do @@ -545,49 +531,39 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, code, :code_1, []) - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 4, state: :evaluating}}} + assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}} - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, - %{ - column: 6, - contents: "\e[31mmatch (=) failed" <> _, - end_line: 4, - line: 4, - state: :failed - }}} - - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 9, state: :evaluating}}} - - assert_receive {:runtime_evaluation_output, :code_1, - { - :doctest_result, - %{ - column: 6, - contents: - "\e[31m** (Protocol.UndefinedError) protocol Enumerable not implemented for 1 of type Integer. " <> - _, - end_line: 10, - line: 9, - state: :failed - } + assert_receive {:runtime_doctest_report, :code_1, + %{ + column: 6, + details: "\e[31mmatch (=) failed" <> _, + end_line: 4, + line: 4, + status: :failed }} - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 17, state: :evaluating}}} + assert_receive {:runtime_doctest_report, :code_1, %{line: 9, status: :running}} - assert_receive {:runtime_evaluation_output, :code_1, - { - :doctest_result, - %{ - column: 6, - contents: "\e[31m** (EXIT from #PID<" <> _, - end_line: 18, - line: 17, - state: :failed - } + assert_receive {:runtime_doctest_report, :code_1, + %{ + column: 6, + details: + "\e[31m** (Protocol.UndefinedError) protocol Enumerable not implemented for 1 of type Integer. " <> + _, + end_line: 10, + line: 9, + status: :failed + }} + + assert_receive {:runtime_doctest_report, :code_1, %{line: 17, status: :running}} + + assert_receive {:runtime_doctest_report, :code_1, + %{ + column: 6, + details: "\e[31m** (EXIT from #PID<" <> _, + end_line: 18, + line: 17, + status: :failed }} end @@ -606,19 +582,16 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, code, :code_1, []) - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, %{line: 4, state: :evaluating}}} + assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}} - assert_receive {:runtime_evaluation_output, :code_1, - {:doctest_result, - %{ - column: 6, - contents: - "\e[31mDoctest did not compile, got: (TokenMissingError) " <> _, - end_line: 5, - line: 4, - state: :failed - }}} + assert_receive {:runtime_doctest_report, :code_1, + %{ + column: 6, + details: "\e[31mDoctest did not compile, got: (TokenMissingError) " <> _, + end_line: 5, + line: 4, + status: :failed + }} end end