From 127ef1c066647a9fea4483a81f33c565ccd23187 Mon Sep 17 00:00:00 2001 From: Simon Lafrance <122399973+SimonLafran@users.noreply.github.com> Date: Sat, 10 Aug 2024 16:39:12 -0400 Subject: [PATCH] Add stale cells (aka single-shot cells) Adds to PlutoRunner a new struct `Stale`. When returned from a cell, this struct passes down its formatting to its single member `out`. However, the cell also gets marked as stale. This means that none of the cells that depend on it will be run. The stale cell, however, will still be run when its dependencies run. On the frontend stale outputs are displayed on a light or dark orange background while stale cells and cells that depend on them get orange traffic lights. When a cell is in one of those two states a warning sign with an explanatory popup is displayed below the "fold code" button. Add a small test for stale cells --- frontend/components/Cell.js | 55 +++++++++++- frontend/components/Editor.js | 4 +- frontend/editor.css | 55 ++++++++---- frontend/themes/dark.css | 2 + frontend/themes/light.css | 46 +++++----- src/evaluation/Run.jl | 82 +++++++++++++---- src/notebook/Cell.jl | 3 + src/runner/PlutoRunner/src/PlutoRunner.jl | 3 + .../PlutoRunner/src/display/format_output.jl | 6 +- src/webserver/Dynamic.jl | 2 + test/runtests.jl | 1 + test/stale_cells.jl | 90 +++++++++++++++++++ 12 files changed, 288 insertions(+), 61 deletions(-) create mode 100644 test/stale_cells.jl diff --git a/frontend/components/Cell.js b/frontend/components/Cell.js index 615b4b8fe3..23891319d1 100644 --- a/frontend/components/Cell.js +++ b/frontend/components/Cell.js @@ -105,7 +105,19 @@ const on_jump = (hasBarrier, pluto_actions, cell_id) => () => { * */ export const Cell = ({ cell_input: { cell_id, code, code_folded, metadata }, - cell_result: { queued, running, runtime, errored, output, logs, published_object_keys, depends_on_disabled_cells, depends_on_skipped_cells }, + cell_result: { + queued, + running, + runtime, + errored, + output, + logs, + published_object_keys, + depends_on_disabled_cells, + depends_on_skipped_cells, + stale, + depends_on_stale_cells, + }, cell_dependencies, cell_input_local, notebook_id, @@ -294,8 +306,10 @@ export const Cell = ({ code_folded, skip_as_script, running_disabled, + stale, depends_on_disabled_cells, depends_on_skipped_cells, + depends_on_stale_cells, show_input, shrunk: Object.values(logs).length > 0, hooked_up: output?.has_pluto_hook_features ?? false, @@ -315,9 +329,42 @@ export const Cell = ({ - +
+ + ${stale + ? html`` + : depends_on_stale_cells + ? html`` + : null} +
${code_not_trusted_yet diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 2afb63b4ea..72cfa94f52 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -118,7 +118,7 @@ const first_true_key = (obj) => { * @type {{ * disabled: boolean, * show_logs: boolean, - * skip_as_script: boolean + * skip_as_script: boolean, * }} * * @typedef CellInputData @@ -160,12 +160,14 @@ const first_true_key = (obj) => { * queued: boolean, * running: boolean, * errored: boolean, + * stale: boolean, * runtime: number?, * downstream_cells_map: { string: [string]}, * upstream_cells_map: { string: [string]}, * precedence_heuristic: number?, * depends_on_disabled_cells: boolean, * depends_on_skipped_cells: boolean, + * depends_on_stale_cells: boolean, * output: { * body: string, * persist_js_state: boolean, diff --git a/frontend/editor.css b/frontend/editor.css index dac3fc7118..5b601a58b2 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -1253,6 +1253,11 @@ pluto-display > div { display: none; } +/* STALE CELLS */ +pluto-cell.stale > pluto-output { + background: var(--pluto-stale-output-bg-color); +} + /* DISABLED CELLS */ pluto-cell.depends_on_disabled_cells > pluto-output, @@ -1390,7 +1395,7 @@ button.floating_back_button, pluto-cell > button, pluto-input > button, pluto-runarea > button, -pluto-shoulder > button, +pluto-shoulder > div > button, nav#slide_controls > button { position: absolute; margin: 0px; @@ -1420,7 +1425,6 @@ pluto-shoulder { --invisible-border: calc(0.5 * var(--pluto-cell-spacing)); --shoulder-width: calc(28px + var(--invisible-border)); --border-radius: calc(5px + var(--invisible-border)); - left: calc(0px - var(--shoulder-width)); width: var(--shoulder-width); border-radius: var(--border-radius) 0px 0px var(--border-radius); @@ -1434,6 +1438,7 @@ pluto-shoulder { bottom: calc(0px - var(--invisible-border)); border: var(--invisible-border) solid rgba(0, 0, 0, 0); border-right: none; + overflow: clip; } pluto-editor.fullscreen pluto-shoulder { --shoulder-width: 2000px; @@ -1444,32 +1449,44 @@ pluto-shoulder:hover { background-clip: padding-box; } -pluto-shoulder > button { +pluto-shoulder > div { flex: 0 0 auto; position: sticky; top: 0; + display: flex; + flex-direction: column; +} + +pluto-shoulder > div > button { + position: static; padding: 4px 5px 4px 10px; } -pluto-cell:focus-within > pluto-shoulder > button { +pluto-cell:focus-within > pluto-shoulder > div > button { /* we use padding instead of 4px extra margin to move the eye to the left so that the hitbox becomes grows - you want to be able to double click the button */ padding-right: 9px; } -/* pluto-cell.code_folded.inline-output > pluto-shoulder > button { +/* pluto-cell.code_folded.inline-output > pluto-shoulder > div > button { margin-top: 3px; } */ -pluto-shoulder > button > span::after { +pluto-shoulder > div > button.foldcode > span::after { background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/eye-outline.svg"); filter: var(--image-filters); } -pluto-cell.code_folded > pluto-shoulder > button > span::after { +pluto-cell.code_folded > pluto-shoulder > div > button.foldcode > span::after { background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/eye-off-outline.svg"); filter: var(--image-filters); } +pluto-shoulder > div > button.stale_cell_marker > span::after, +pluto-shoulder > div > button.depends_on_stale_cells_marker > span::after { + background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/warning-outline.svg"); + filter: var(--image-filters); +} + /* TRAFFIC LIGHT */ pluto-trafficlight { @@ -1548,6 +1565,14 @@ body:not(.___) pluto-cell.errored > pluto-trafficlight { background-clip: padding-box; } +/* stale cells */ +body:not(.___) pluto-cell.stale > pluto-trafficlight, +body:not(.___) pluto-cell.depends_on_stale_cells > pluto-trafficlight { + background: var(--stale-cell-color); + border-left-color: var(--stale-cell-color); + background-clip: padding-box; +} + body:not(.___) pluto-cell.queued > pluto-trafficlight::after { background: repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0) 8px, var(--normal-cell-color) 8px, var(--normal-cell-color) 16px); background-clip: padding-box; @@ -1624,7 +1649,7 @@ pluto-input > button > span { pluto-cell > button, pluto-input > button, pluto-runarea > button, - pluto-shoulder > button, + pluto-shoulder > div > button, pluto-cell > pluto-runarea { opacity: 0; /* to make it feel smooth: */ @@ -1638,8 +1663,8 @@ pluto-input > button > span { pluto-cell:hover > pluto-input > button, pluto-cell:focus-within > pluto-input > button, pluto-cell > pluto-runarea > button, - pluto-cell:hover > pluto-shoulder > button, - pluto-cell:focus-within > pluto-shoulder > button { + pluto-cell:hover > pluto-shoulder > div > button, + pluto-cell:focus-within > pluto-shoulder > div > button { opacity: 0.6; transition: opacity 0.25s ease-in-out; } @@ -1650,7 +1675,7 @@ pluto-input > button > span { pluto-cell > button:hover, pluto-cell > pluto-input > button:hover, pluto-cell > pluto-runarea > button:hover, - pluto-cell > pluto-shoulder > button:hover, + pluto-cell > pluto-shoulder > div > button:hover, pluto-cell:hover > pluto-runarea { opacity: 1; /* to make it feel snappy: */ @@ -1661,7 +1686,7 @@ pluto-input > button > span { @media screen and (pointer: coarse) { pluto-cell > button.add_cell, pluto-input > button, - pluto-shoulder > button { + pluto-shoulder > div > button { opacity: 0.25; transition: opacity 0.25s ease-in-out; } @@ -1676,14 +1701,14 @@ pluto-input > button > span { pluto-cell:focus-within > button.add_cell, pluto-cell:focus-within > pluto-input > button, pluto-cell:focus-within > pluto-runarea, - pluto-cell:focus-within > pluto-shoulder > button { + pluto-cell:focus-within > pluto-shoulder > div > button { opacity: 0.6; transition: opacity 0.25s ease-in-out; } pluto-cell > pluto-input > button:focus-within, pluto-cell > button:focus-within, pluto-cell > pluto-input > button:focus-within pluto-cell > pluto-runarea > button:focus-within, - pluto-cell > pluto-shoulder > button:focus-within, + pluto-cell > pluto-shoulder > div > button:focus-within, pluto-cell > pluto-runarea { opacity: 1; /* to make it feel snappy: */ @@ -1694,7 +1719,7 @@ pluto-input > button > span { pluto-cell > button > span::after, pluto-input > button > span::after, pluto-runarea > button > span::after, -pluto-shoulder > button > span::after { +pluto-shoulder > div > button > span::after { display: block; content: " " !important; background-size: 17px 17px; diff --git a/frontend/themes/dark.css b/frontend/themes/dark.css index d72c9ea39c..b3b14b3096 100644 --- a/frontend/themes/dark.css +++ b/frontend/themes/dark.css @@ -23,6 +23,7 @@ --error-cell-color: rgba(var(--error-color), 0.6); --bright-error-cell-color: rgba(var(--error-color), 0.9); --light-error-cell-color: rgba(var(--error-color), 0); + --stale-cell-color: #b86f00; /*Export styling*/ --export-bg-color: hsl(225deg 17% 18%); @@ -45,6 +46,7 @@ --pluto-output-color: hsl(0deg 0% 77%); --pluto-output-h-color: hsl(0, 0%, 90%); --pluto-output-bg-color: var(--main-bg-color); + --pluto-stale-output-bg-color: #332300; --a-underline: #ffffff69; --blockquote-color: inherit; --blockquote-bg: #2e2e2e; diff --git a/frontend/themes/light.css b/frontend/themes/light.css index c1bd8b4f03..4a4dd51ed4 100644 --- a/frontend/themes/light.css +++ b/frontend/themes/light.css @@ -23,6 +23,7 @@ --error-cell-color: rgba(var(--error-color), 0.7); --bright-error-cell-color: rgb(var(--error-color)); --light-error-cell-color: rgba(var(--error-color), 0.05); + --stale-cell-color: #ffba53; /*Export styling*/ --export-bg-color: rgb(60, 67, 101); @@ -45,6 +46,7 @@ --pluto-output-color: hsl(0, 0%, 25%); --pluto-output-h-color: hsl(0, 0%, 12%); --pluto-output-bg-color: white; + --pluto-stale-output-bg-color: #ffeed5; --a-underline: #00000059; --blockquote-color: #555; --blockquote-bg: #f2f2f2; @@ -191,36 +193,36 @@ /* code highlighting */ --cm-color-editor-text: #41323f; - --cm-color-comment: #e96ba8; - --cm-color-atom: #815ba4; - --cm-color-number: #815ba4; - --cm-color-property: #b67a48; - --cm-color-keyword: #ef6155; - --cm-color-string: #da5616; - --cm-color-var: #5668a4; - --cm-color-var2: #37768a; - --cm-color-macro: #5c8c5f; - --cm-color-builtin: #5e7ad3; - --cm-color-function: #cc80ac; - --cm-color-type: hsl(170deg 7% 56%); - --cm-color-bracket: #41323f; - --cm-color-tag: #ef6155; - --cm-color-link: #815ba4; - --cm-color-error-bg: #ef6155; - --cm-color-error: #f7f7f7; + --cm-color-comment: #e96ba8; + --cm-color-atom: #815ba4; + --cm-color-number: #815ba4; + --cm-color-property: #b67a48; + --cm-color-keyword: #ef6155; + --cm-color-string: #da5616; + --cm-color-var: #5668a4; + --cm-color-var2: #37768a; + --cm-color-macro: #5c8c5f; + --cm-color-builtin: #5e7ad3; + --cm-color-function: #cc80ac; + --cm-color-type: hsl(170deg 7% 56%); + --cm-color-bracket: #41323f; + --cm-color-tag: #ef6155; + --cm-color-link: #815ba4; + --cm-color-error-bg: #ef6155; + --cm-color-error: #f7f7f7; --cm-color-matchingBracket: black; --cm-color-matchingBracket-bg: #1b4bbb21; --cm-color-placeholder-text: rgba(0, 0, 0, 0.2); --cm-color-clickable-underline: #ced2ef; /* Mixed parsers */ - --cm-color-html: #48b685; + --cm-color-html: #48b685; --cm-color-html-accent: #00ab85; - --cm-color-css: #876800; - --cm-color-css-accent: #696200; + --cm-color-css: #876800; + --cm-color-css-accent: #696200; --cm-css-why-doesnt-codemirror-highlight-all-the-text-aaa: #3b3700; - --cm-color-md: #005a9b; - --cm-color-md-accent: #00a9d1; + --cm-color-md: #005a9b; + --cm-color-md-accent: #00a9d1; /*autocomplete menu*/ --autocomplete-menu-bg-color: white; diff --git a/src/evaluation/Run.jl b/src/evaluation/Run.jl index ae1d223578..43bc6383dc 100644 --- a/src/evaluation/Run.jl +++ b/src/evaluation/Run.jl @@ -75,6 +75,8 @@ function run_reactive_core!( # find (indirectly) skipped-as-script cells and update their status update_skipped_cells_dependency!(notebook, new_topology) + update_stale_cell_dependents!(notebook, new_topology) + removed_cells = setdiff(all_cells(old_topology), all_cells(new_topology)) roots = vcat(roots, removed_cells) @@ -182,22 +184,29 @@ function run_reactive_core!( if (skip = any_interrupted || notebook.wants_to_interrupt || !will_run_code(notebook)) relay_reactivity_error!(cell, InterruptException()) - else - run = run_single!( - (session, notebook), cell, - new_topology.nodes[cell], new_topology.codes[cell]; - user_requested_run = (user_requested_run && cell ∈ roots), - capture_stdout = session.options.evaluation.capture_stdout, - ) - any_interrupted |= run.interrupted - - # Support one bond defining another when setting both simultaneously in PlutoSliderServer - # https://github.com/fonsp/Pluto.jl/issues/1695 - - # set the redefined bound variables to their original value from the request - defs = notebook.topology.nodes[cell].definitions - set_bond_value_pairs!(session, notebook, Iterators.filter(((sym,val),) -> sym ∈ defs, bond_value_pairs)) - end + elseif !(skip = cell.depends_on_stale_cells) + was_stale = cell.stale + + run = run_single!( + (session, notebook), cell, + new_topology.nodes[cell], new_topology.codes[cell]; + user_requested_run=(user_requested_run && cell ∈ roots), + capture_stdout=session.options.evaluation.capture_stdout, + ) + any_interrupted |= run.interrupted + + # Support one bond defining another when setting both simultaneously in PlutoSliderServer + # https://github.com/fonsp/Pluto.jl/issues/1695 + + # set the redefined bound variables to their original value from the request + defs = notebook.topology.nodes[cell].definitions + set_bond_value_pairs!(session, notebook, Iterators.filter(((sym, val),) -> sym ∈ defs, bond_value_pairs)) + + # stale status changed + if was_stale != run.stale + update_stale_cell_dependents!(notebook, new_topology) + end + end cell.running = false Status.report_business_finished!(cell_status, Symbol(i), !skip && !run.errored) @@ -331,7 +340,8 @@ function set_output!(cell::Cell, run, expr_cache::ExprAnalysisCache; persist_js_ end new_published end - + + cell.stale = run.stale cell.runtime = run.runtime cell.errored = run.errored cell.running = cell.queued = false @@ -340,7 +350,8 @@ end function clear_output!(cell::Cell) cell.output = CellOutput() cell.published_objects = Dict{String,Any}() - + + cell.stale = false cell.runtime = nothing cell.errored = false cell.running = cell.queued = false @@ -663,3 +674,38 @@ function update_disabled_cells_dependency!(notebook::Notebook, topology::Noteboo cell.depends_on_disabled_cells = true end end + +""" +Find all runnable cells that depend on stale cells. This does not include the +stale cells themselves unless they themselves depend on stale cells. +""" +function find_stale_cell_dependents(notebook::Notebook, topology::NotebookTopology=notebook.topology)::Set{Cell} + stack = collect(filter(stale, notebook.cells)) + dependents = Set{Cell}() + # depth-first search + while !isempty(stack) + cell = pop!(stack) + + cell_dependents = where_referenced(topology, cell) + + new_dependents = setdiff(cell_dependents, dependents) + union!(dependents, new_dependents) + append!(stack, new_dependents) + end + + dependents +end + +""" +Updates `depends_on_stale_cells` for the cells in the notebook. This does not include the +stale cells themselves unless they themselves depend on stale cells. +""" +function update_stale_cell_dependents!(notebook::Notebook, topology::NotebookTopology=notebook.topology) + for cell in notebook.cells + cell.depends_on_stale_cells = false + end + + for cell in find_stale_cell_dependents(notebook, topology) + cell.depends_on_stale_cells = true + end +end diff --git a/src/notebook/Cell.jl b/src/notebook/Cell.jl index 5af2d4a27c..6f263e47f0 100644 --- a/src/notebook/Cell.jl +++ b/src/notebook/Cell.jl @@ -43,6 +43,7 @@ Base.@kwdef mutable struct Cell <: PlutoDependencyExplorer.AbstractCell output::CellOutput=CellOutput() queued::Bool=false running::Bool=false + stale::Bool=false published_objects::Dict{String,Any}=Dict{String,Any}() @@ -56,6 +57,7 @@ Base.@kwdef mutable struct Cell <: PlutoDependencyExplorer.AbstractCell depends_on_disabled_cells::Bool=false depends_on_skipped_cells::Bool=false + depends_on_stale_cells::Bool=false metadata::Dict{String,Any}=copy(DEFAULT_CELL_METADATA) end @@ -84,3 +86,4 @@ end can_show_logs(c::Cell) = get(c.metadata, METADATA_SHOW_LOGS_KEY, DEFAULT_CELL_METADATA[METADATA_SHOW_LOGS_KEY]) is_skipped_as_script(c::Cell) = get(c.metadata, METADATA_SKIP_AS_SCRIPT_KEY, DEFAULT_CELL_METADATA[METADATA_SKIP_AS_SCRIPT_KEY]) must_be_commented_in_file(c::Cell) = is_disabled(c) || is_skipped_as_script(c) || c.depends_on_disabled_cells || c.depends_on_skipped_cells +stale(c::Cell) = c.stale diff --git a/src/runner/PlutoRunner/src/PlutoRunner.jl b/src/runner/PlutoRunner/src/PlutoRunner.jl index 4e449833a2..f97b28d999 100644 --- a/src/runner/PlutoRunner/src/PlutoRunner.jl +++ b/src/runner/PlutoRunner/src/PlutoRunner.jl @@ -30,6 +30,9 @@ abstract type SpecialPlutoExprValue end struct GiveMeCellID <: SpecialPlutoExprValue end struct GiveMeRerunCellFunction <: SpecialPlutoExprValue end struct GiveMeRegisterCleanupFunction <: SpecialPlutoExprValue end +struct Stale + out +end # TODO: clear key when a cell is deleted furever diff --git a/src/runner/PlutoRunner/src/display/format_output.jl b/src/runner/PlutoRunner/src/display/format_output.jl index 672d5786bf..8a62eeb49c 100644 --- a/src/runner/PlutoRunner/src/display/format_output.jl +++ b/src/runner/PlutoRunner/src/display/format_output.jl @@ -14,7 +14,8 @@ const table_column_display_limit_increase = 30 const tree_display_extra_items = Dict{UUID,Dict{ObjectDimPair,Int64}}() # This is not a struct to make it easier to pass these objects between processes. -const FormattedCellResult = NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects, :has_pluto_hook_features),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any},Bool}} +const FormattedCellResult = NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects, :has_pluto_hook_features, :stale),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any},Bool,Bool}} + function formatted_result_of( notebook_id::UUID, @@ -39,6 +40,7 @@ function formatted_result_of( has_pluto_hook_features = haskey(cell_expanded_exprs, cell_id) && cell_expanded_exprs[cell_id].has_pluto_hook_features ans = cell_results[cell_id] errored = ans isa CapturedException + stale = ans isa Stale output_formatted = if (!ends_with_semicolon || errored) with_logger_and_io_to_logs(get_cell_logger(notebook_id, cell_id); capture_stdout) do @@ -70,6 +72,7 @@ function formatted_result_of( runtime = get(cell_runtimes, cell_id, nothing), published_objects, has_pluto_hook_features, + stale, ) end @@ -102,6 +105,7 @@ format_output(@nospecialize(x); context=default_iocontext) = format_output_defau format_output(::Nothing; context=default_iocontext) = ("", MIME"text/plain"()) +format_output((;out)::Stale; context=default_iocontext) = format_output(out; context) function format_output(binding::Base.Docs.Binding; context=default_iocontext) diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 5960c0ff74..cda79d0432 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -124,9 +124,11 @@ function notebook_to_js(notebook::Notebook) "queued" => cell.queued, "running" => cell.running, "errored" => cell.errored, + "stale" => cell.stale, "runtime" => cell.runtime, "logs" => FirebaseyUtils.AppendonlyMarker(cell.logs), "depends_on_skipped_cells" => cell.depends_on_skipped_cells, + "depends_on_stale_cells" => cell.depends_on_stale_cells, ) for (id, cell) in notebook.cells_dict), "cell_order" => notebook.cell_order, diff --git a/test/runtests.jl b/test/runtests.jl index 83b2bae1b1..c9066c6bc4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,6 +45,7 @@ verify_no_running_processes() @timeit_include("DependencyCache.jl") @timeit_include("Throttled.jl") @timeit_include("cell_disabling.jl") +@timeit_include("stale_cells.jl") verify_no_running_processes() diff --git a/test/stale_cells.jl b/test/stale_cells.jl new file mode 100644 index 0000000000..a7731fd185 --- /dev/null +++ b/test/stale_cells.jl @@ -0,0 +1,90 @@ +using Test +using Pluto +using Pluto: update_run!, ServerSession, Cell, Notebook, WorkspaceManager + +@testset "Stale cells" begin + 🍭 = ServerSession() + 🍭.options.evaluation.workspace_use_distributed = false + + notebook = Notebook([ + Cell("a = 1") # 1 + Cell("b = a + 1") # 2 + + Cell(""" + begin + c = b + push!(runs, a + 1) + b + end + """) # 3 + + Cell("a + 1") # 4 + + Cell("begin runs = [] end") # 5 + + Cell("length(runs)") # 6 + + Cell("c") # 7 + ]) + update_run!(🍭, notebook, notebook.cells) + + id(i) = notebook.cells[i].cell_id + c(i) = notebook.cells[i] + get_depends_on_stale_cells(notebook::Notebook) = [i for (i, c) in pairs(notebook.cells) if c.depends_on_stale_cells] + get_stale_cells(notebook::Notebook) = [i for (i, c) in pairs(notebook.cells) if c.stale] + + @test get_stale_cells(notebook) == [] + @test get_depends_on_stale_cells(notebook) == [] + @test all(noerror, notebook.cells) + @test c(6).output.body == "1" + + # Cell metadata gets updated + setcode!(c(2), "begin b = a + 1; Main.PlutoRunner.Stale(Nothing) end") + + update_run!(🍭, notebook, c(2)) + + @test get_stale_cells(notebook) == [2] + @test get_depends_on_stale_cells(notebook) == [3, 7] + @test all(noerror, notebook.cells) + + # depends_on_stale_cells output does not change, but stale cells do + setcode!(c(2), "begin b = a + 2; Main.PlutoRunner.Stale(b) end") + + update_run!(🍭, notebook, c(2)) + update_run!(🍭, notebook, c(6)) + + @test get_stale_cells(notebook) == [2] + @test get_depends_on_stale_cells(notebook) == [3, 7] + @test c(3).output.body == "2" + @test c(2).output.body == "3" + @test all(noerror, notebook.cells) + @test c(6).output.body == "1" + + setcode!(c(1), "a = 2") + + update_run!(🍭, notebook, c(1)) + update_run!(🍭, notebook, c(6)) + + @test get_stale_cells(notebook) == [2] + @test get_depends_on_stale_cells(notebook) == [3, 7] + @test c(3).output.body == "2" + @test c(2).output.body == "4" + @test c(7).output.body == "2" + @test all(noerror, notebook.cells) + @test c(6).output.body == "1" + + # we can resume + setcode!(c(2), "begin b = a + 2; Nothing end") + + update_run!(🍭, notebook, c(2)) + update_run!(🍭, notebook, c(6)) + + @test get_stale_cells(notebook) == [] + @test get_depends_on_stale_cells(notebook) == [] + @test c(3).output.body == "4" + @test c(7).output.body == "4" + @test all(noerror, notebook.cells) + @test c(6).output.body == "2" + + cleanup(🍭, notebook) +end \ No newline at end of file