diff --git a/Project.toml b/Project.toml index e77929ac0b..ea782e61a4 100644 --- a/Project.toml +++ b/Project.toml @@ -2,9 +2,10 @@ name = "Pluto" uuid = "c3e4b0f8-55cb-11ea-2926-15256bba5781" license = "MIT" authors = ["Fons van der Plas ", "Mikołaj Bochenski "] -version = "0.4.3" +version = "0.5.0" [deps] +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" diff --git a/src/Pluto.jl b/src/Pluto.jl index a631e88aa1..1c0f96df0a 100644 --- a/src/Pluto.jl +++ b/src/Pluto.jl @@ -14,6 +14,7 @@ const VERSION_STR = 'v' * Pkg.TOML.parsefile(joinpath(PKG_ROOT_DIR, "Project.tom \n""" include("./react/ExploreExpression.jl") +include("./webserver/FormatOutput.jl") using .ExploreExpression include("./react/Cell.jl") include("./react/Notebook.jl") @@ -22,7 +23,6 @@ include("./react/React.jl") include("./webserver/NotebookServer.jl") include("./webserver/Static.jl") -include("./webserver/FormatOutput.jl") include("./webserver/Dynamic.jl") end \ No newline at end of file diff --git a/src/react/Cell.jl b/src/react/Cell.jl index 55aca66e99..d3a3802501 100644 --- a/src/react/Cell.jl +++ b/src/react/Cell.jl @@ -1,43 +1,26 @@ using UUIDs - "The building block of `Notebook`s. Contains both code and output." mutable struct Cell "because Cells can be reordered, they get a UUID. The JavaScript frontend indexes cells using the UUID." uuid::UUID code::String parsedcode::Any - output::Any + output_repr::Union{String, Nothing} + error_repr::Union{String, Nothing} + repr_mime::MIME runtime::Union{Missing,UInt64} - errormessage::Any symstate::SymbolsState resolved_funccalls::Set{Symbol} resolved_symstate::SymbolsState module_usings::Set{Expr} end -Cell(uuid, code) = Cell(uuid, code, nothing, nothing, missing, nothing, SymbolsState(), Set{Symbol}(), SymbolsState(), Set{Expr}()) +Cell(uuid, code) = Cell(uuid, code, nothing, nothing, nothing, MIME("text/plain"), missing, SymbolsState(), Set{Symbol}(), SymbolsState(), Set{Expr}()) "Turn a `Cell` into an object that can be serialized using `JSON.json`, to be sent to the client." function serialize(cell::Cell) - Dict(:uuid => string(cell.uuid), :code => cell.code)# , :output => cell.output) -end - -createcell_fromcode(code::String) = Cell(uuid1(), code) - -function relay_output!(cell::Cell, output::Any) - cell.output = output - cell.errormessage = nothing -end - -function relay_error!(cell::Cell, message::String) - cell.output = nothing - cell.errormessage = message + Dict(:uuid => string(cell.uuid), :code => cell.code) end -relay_error!(cell::Cell, err::Exception) = relay_error!(cell, sprint(showerror, err)) -function relay_error!(cell::Cell, err::Exception, backtrace::Array{Base.StackTraces.StackFrame,1}) - until = findfirst(sf -> sf.func == :run_single!, backtrace) - backtrace_trimmed = until === nothing ? backtrace : backtrace[1:until-1] - relay_error!(cell, sprint(showerror, err, backtrace_trimmed)) -end \ No newline at end of file +createcell_fromcode(code::String) = Cell(uuid1(), code) \ No newline at end of file diff --git a/src/react/Notebook.jl b/src/react/Notebook.jl index b03345ddd5..5508be3e44 100644 --- a/src/react/Notebook.jl +++ b/src/react/Notebook.jl @@ -11,11 +11,17 @@ mutable struct Notebook # buffer must contain all undisplayed outputs pendingupdates::Channel + + executetoken::Channel end # We can keep 128 updates pending. After this, any put! calls (i.e. calls that push an update to the notebook) will simply block, which is fine. # This does mean that the Notebook can't be used if nothing is clearing the update channel. -Notebook(path::String, cells::Array{Cell,1}, uuid) = Notebook(path, cells, uuid, nothing, Channel(128)) -Notebook(path::String, cells::Array{Cell,1}) = Notebook(path, cells, uuid4(), nothing, Channel(128)) +Notebook(path::String, cells::Array{Cell,1}, uuid) = let + et = Channel{Nothing}(1) + put!(et, nothing) + Notebook(path, cells, uuid, nothing, Channel(128), et) +end +Notebook(path::String, cells::Array{Cell,1}) = Notebook(path, cells, uuid1()) function selectcell_byuuid(notebook::Notebook, uuid::UUID)::Union{Cell,Nothing} cellIndex = findfirst(c->c.uuid == uuid, notebook.cells) diff --git a/src/react/React.jl b/src/react/React.jl index 6676bf9b95..caa124de83 100644 --- a/src/react/React.jl +++ b/src/react/React.jl @@ -1,30 +1,57 @@ +import Base: showerror + +abstract type ReactivityError <: Exception end + +struct CircularReferenceError <: ReactivityError + syms::Set{Symbol} +end + +struct MultipleDefinitionsError <: ReactivityError + syms::Set{Symbol} +end + +function showerror(io::IO, cre::CircularReferenceError) + print(io, "Circular references among $(join(cre.syms, ", ", " and ")).") +end + +function showerror(io::IO, mde::MultipleDefinitionsError) + print(io, "Multiple definitions for $(join(mde.syms, ", ", " and ")).\nCombine all definitions into a single reactive cell using a `begin` ... `end` block.") # TODO: hint about mutable globals +end + + +"Sends `error` to the frontend without backtrace. Runtime errors are handled by `WorkspaceManager.eval_fetch_in_workspace` - this function is for Reactivity errors." +function relay_reactivity_error!(cell::Cell, error::Exception) + cell.output_repr = nothing + cell.error_repr, cell.repr_mime = format_output(error) +end + + function run_single!(initiator, notebook::Notebook, cell::Cell) - workspace = WorkspaceManager.get_workspace(notebook) starttime = time_ns() - try - # deleted_refs = setdiff(cell.resolved_symstate.references, cell.resolved_symstate.assignments) ∩ workspace.deleted_vars - deleted_refs = cell.resolved_symstate.references ∩ workspace.deleted_vars - if !isempty(deleted_refs) - deleted_refs |> first |> UndefVarError |> throw - end - starttime = time_ns() - output = Core.eval(workspace.workspace_module, cell.parsedcode) - cell.runtime = time_ns() - starttime - - relay_output!(cell, output) + output, errored = WorkspaceManager.eval_fetch_in_workspace(notebook, cell.parsedcode) + cell.runtime = time_ns() - starttime + if errored + cell.output_repr = nothing + cell.error_repr = output[1] + cell.repr_mime = output[2] + else + cell.output_repr = output[1] + cell.error_repr = nothing + cell.repr_mime = output[2] WorkspaceManager.undelete_vars(notebook, cell.resolved_symstate.assignments) - # TODO: capture stdout and display it somehwere, but let's keep using the actual terminal for now - catch err - cell.runtime = time_ns() - starttime - bt = stacktrace(catch_backtrace()) - relay_error!(cell, err, bt) end + # TODO: capture stdout and display it somehwere, but let's keep using the actual terminal for now end "Run a cell and all the cells that depend on it" function run_reactive!(initiator, notebook::Notebook, cell::Cell) + # This guarantees that we are the only run_reactive! that is running cells right now: + token = take!(notebook.executetoken) + + workspace = WorkspaceManager.get_workspace(notebook) + cell.parsedcode = Meta.parse(cell.code, raise=false) cell.module_usings = ExploreExpression.compute_usings(cell.parsedcode) @@ -96,30 +123,45 @@ function run_reactive!(initiator, notebook::Notebook, cell::Cell) (keys(c.resolved_symstate.funcdefs) for c in will_update)... ) - WorkspaceManager.delete_vars(notebook, to_delete_vars) - WorkspaceManager.delete_funcs(notebook, to_delete_funcs) + WorkspaceManager.delete_vars(workspace, to_delete_vars) + WorkspaceManager.delete_funcs(workspace, to_delete_funcs) for to_run in will_update - if to_run in reassigned - assigned_multiple = let - other_modifiers = setdiff(competing_modifiers, [to_run]) - union((to_run.resolved_symstate.assignments ∩ c.resolved_symstate.assignments for c in other_modifiers)...) - end - relay_error!(to_run, "Multiple definitions for $(join(assigned_multiple, ", ", " and "))") - elseif to_run in cyclic - assigned_cyclic = let - referenced_during_cycle = union((c.resolved_symstate.references for c in cyclic)...) - assigned_during_cycle = union((c.resolved_symstate.assignments for c in cyclic)...) - - referenced_during_cycle ∩ assigned_during_cycle - end - relay_error!(to_run, "Cyclic references: $(join(assigned_cyclic, ", ", " and "))") + assigned_multiple = if to_run in reassigned + other_modifiers = setdiff(competing_modifiers, [to_run]) + union((to_run.resolved_symstate.assignments ∩ c.resolved_symstate.assignments for c in other_modifiers)...) + else + [] + end + + assigned_cyclic = if to_run in cyclic + referenced_during_cycle = union((c.resolved_symstate.references for c in cyclic)...) + assigned_during_cycle = union((c.resolved_symstate.assignments for c in cyclic)...) + + referenced_during_cycle ∩ assigned_during_cycle + else + [] + end + + deleted_refs = let + to_run.resolved_symstate.references ∩ workspace.deleted_vars + end + + if length(assigned_multiple) > 0 + relay_reactivity_error!(to_run, assigned_multiple |> MultipleDefinitionsError) + elseif length(assigned_cyclic) > 1 + relay_reactivity_error!(to_run, assigned_cyclic |> CircularReferenceError) + elseif length(deleted_refs) > 0 + relay_reactivity_error!(to_run, deleted_refs |> first |> UndefVarError) else run_single!(initiator, notebook, to_run) end + putnotebookupdates!(notebook, clientupdate_cell_output(initiator, notebook, to_run)) end + put!(notebook.executetoken, token) + return will_update end diff --git a/src/react/WorkspaceManager.jl b/src/react/WorkspaceManager.jl index d6b52a3cd7..d48e8c8294 100644 --- a/src/react/WorkspaceManager.jl +++ b/src/react/WorkspaceManager.jl @@ -1,24 +1,40 @@ module WorkspaceManager import UUIDs: UUID -import ..Pluto: Notebook +import ..Pluto: Notebook, PKG_ROOT_DIR, format_output +import Distributed +abstract type AbstractWorkspace end -mutable struct Workspace +mutable struct ModuleWorkspace <: AbstractWorkspace name::Symbol workspace_module::Module deleted_vars::Set{Symbol} end +mutable struct ProcessWorkspace <: AbstractWorkspace + workspace_pid::Int64 + deleted_vars::Set{Symbol} +end + "These expressions get executed whenever a new workspace is created." workspace_preamble = [:(using Markdown), :(ENV["GKSwstype"] = "nul")] -workspace_count = 0 -workspaces = Dict{UUID, Workspace}() +moduleworkspace_count = 0 +workspaces = Dict{UUID, AbstractWorkspace}() +default_workspace_method = Ref(ProcessWorkspace) -function make_workspace(notebook::Notebook) - global workspace_count += 1 - id = workspace_count +"The workspace method to be used for all future workspace creations. ModuleWorkspace` is lightest, `ProcessWorkspace` can always terminate." +function set_default_workspace_method(method) + default_workspace_method[] = method +end + +"Create a workspace for the notebook using the `default_workspace_method`." +make_workspace(notebook::Notebook)::AbstractWorkspace = make_workspace(notebook, Val(default_workspace_method[])) + +function make_workspace(notebook::Notebook, ::Val{ModuleWorkspace})::ModuleWorkspace + global moduleworkspace_count += 1 + id = moduleworkspace_count new_workspace_name = Symbol("workspace", id) workspace_creation = :(module $(new_workspace_name) $(workspace_preamble...) end) @@ -38,12 +54,42 @@ function make_workspace(notebook::Notebook) close(wr) close(rd) - workspace = Workspace(new_workspace_name, m, Set{Symbol}()) + workspace = ModuleWorkspace(new_workspace_name, m, Set{Symbol}()) + workspaces[notebook.uuid] = workspace + workspace +end + +function make_workspace(notebook::Notebook, ::Val{ProcessWorkspace})::ProcessWorkspace + pid = Distributed.addprocs(1) |> first + + workspace = ProcessWorkspace(pid, Set{Symbol}()) workspaces[notebook.uuid] = workspace + + eval_in_workspace.([workspace], workspace_preamble) + # TODO: we could also import Pluto + eval_in_workspace(workspace, :(include($(joinpath(PKG_ROOT_DIR, "src", "webserver", "FormatOutput.jl"))))) + workspace end -function get_workspace(notebook::Notebook)::Workspace +"Try our best to delete the workspace. `ProcessWorkspace` will have its worker process terminated." +unmake_workspace(notebook::Notebook) = unmake_workspace(get_workspace(notebook)) + +function unmake_workspace(workspace::ProcessWorkspace) + # TODO: verify that nothing is running + Distributed.rmprocs([workspace.workspace_pid]) +end + +function unmake_workspace(workspace::ModuleWorkspace) + # TODO: test + for s in names(workspace.workspace_module) + try + Core.eval(workspace.workspace_module, :($s = nothing)) + catch end + end +end + +function get_workspace(notebook::Notebook)::AbstractWorkspace if haskey(workspaces, notebook.uuid) workspaces[notebook.uuid] else @@ -51,11 +97,65 @@ function get_workspace(notebook::Notebook)::Workspace end end +"Evaluate expression inside the workspace - output is fetched and formatted, errors are caught and formatted. Returns formatted output and errored? flag." +function eval_fetch_in_workspace(notebook::Notebook, expr)::Tuple{Tuple{String, MIME}, Bool} + eval_fetch_in_workspace(get_workspace(notebook), expr) +end + +function eval_fetch_in_workspace(workspace::ProcessWorkspace, expr)::Tuple{Tuple{String, MIME}, Bool} + # We wrap the expression in a try-catch block, because we want to capture and format the exception on the worker itself. + wrapped = :(ans = try + # We want to eval `expr` in the global scope, try introduced a local scope. + Core.eval(Main, $(expr |> QuoteNode)) + catch ex + bt = stacktrace(catch_backtrace()) + CapturedException(ex, bt) + end) + # run the code 🏃‍♀️ + # we use [pid] instead of pid to prevent fetching output + Distributed.remotecall_eval(Main, [workspace.workspace_pid], wrapped) + + # instead of fetching the output value (which might not make sense in our context, since the user can define structs, types, functions, etc), we format the cell output on the worker, and fetch the formatted output. + # This also means that very big objects are not duplicated in RAM. + Distributed.remotecall_eval(Main, workspace.workspace_pid, :(format_output(ans), isa(ans, CapturedException))) +end + +function eval_fetch_in_workspace(workspace::ModuleWorkspace, expr)::Tuple{Tuple{String, MIME}, Bool} + ans = try + Core.eval(workspace.workspace_module, expr) + catch ex + bt = stacktrace(catch_backtrace()) + CapturedException(ex, bt) + end + + format_output(ans), isa(ans, CapturedException) +end + +"Evaluate expression inside the workspace - output is not fetched, errors are rethrown." +function eval_in_workspace(notebook::Notebook, expr) + eval_in_workspace(get_workspace(notebook), expr) +end + +function eval_in_workspace(workspace::ProcessWorkspace, expr) + Distributed.remotecall_eval(Main, [workspace.workspace_pid], expr) + nothing +end + +function eval_in_workspace(workspace::ModuleWorkspace, expr) + Core.eval(workspace.workspace_module, expr) + nothing +end + + +"Delete all methods of the functions from the workspace." function delete_funcs(notebook::Notebook, to_delete::Set{Symbol}) - ws = get_workspace(notebook) + delete_funcs(get_workspace(notebook), to_delete) +end + +function delete_funcs(workspace::ModuleWorkspace, to_delete::Set{Symbol}) for funcname in to_delete try - func = Core.eval(ws.workspace_module, funcname) + func = Core.eval(workspace.workspace_module, funcname) for m in methods(func).ms Base.delete_method(m) end @@ -67,13 +167,40 @@ function delete_funcs(notebook::Notebook, to_delete::Set{Symbol}) end end +function delete_funcs(workspace::ProcessWorkspace, to_delete::Set{Symbol}) + isempty(to_delete) && return + e = :(for funcname in $to_delete + try + func = Core.eval(Main, funcname) + for m in methods(func).ms + Base.delete_method(m) + end + catch ex + if !(ex isa UndefVarError) + rethrow(ex) + end + end + end) + eval_in_workspace(workspace, e) +end + +"Fake deleting variables by adding them to the workspace's blacklist." function delete_vars(notebook::Notebook, to_delete::Set{Symbol}) - ws = get_workspace(notebook) - ws.deleted_vars = ws.deleted_vars ∪ to_delete + delete_vars(get_workspace(notebook), to_delete) end +function delete_vars(workspace, to_delete::Set{Symbol}) + workspace.deleted_vars = workspace.deleted_vars ∪ to_delete +end + +"Remove variables for the workspace's blacklist." function undelete_vars(notebook::Notebook, to_undelete::Set{Symbol}) - ws = get_workspace(notebook) - ws.deleted_vars = setdiff(ws.deleted_vars, to_undelete) + undelete_vars(get_workspace(notebook), to_undelete) +end + +function undelete_vars(workspace, to_undelete::Set{Symbol}) + workspace.deleted_vars = setdiff(workspace.deleted_vars, to_undelete) end + + end \ No newline at end of file diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 58422e4eb6..4d4993027c 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -36,12 +36,12 @@ end function clientupdate_cell_output(initiator::Client, notebook::Notebook, cell::Cell) - payload, mime = format_output(cell.output) + payload, mime = cell.output_repr, cell.repr_mime return UpdateMessage(:cell_output, Dict(:mime => mime, :output => payload, - :errormessage => cell.errormessage, + :errormessage => cell.error_repr, :runtime => cell.runtime, ), notebook, cell, initiator) diff --git a/src/webserver/FormatOutput.jl b/src/webserver/FormatOutput.jl index 232901e590..4777bbd670 100644 --- a/src/webserver/FormatOutput.jl +++ b/src/webserver/FormatOutput.jl @@ -1,3 +1,5 @@ +# will be executed on workspace process + using Markdown import Base: show import Markdown: html, htmlinline, LaTeX, withtag, htmlesc @@ -44,4 +46,25 @@ function format_output(val::Any)::Tuple{String, MIME} Base.invokelatest(repr, mime, ex; context = iocontext), mime end end +end + +function format_output(val::CapturedException)::Tuple{String, MIME} + # in order of coolness + # text/plain always matches + mime = MIME("text/html") + + bt = val.processed_bt + + until = findfirst(b -> b[1].func == :eval, reverse(bt)) + bt = until === nothing ? bt : bt[1:(length(bt) - until)] + + sprint(showerror, val.ex, bt), mime +end + +function format_output(ex::Exception)::Tuple{String, MIME} + # in order of coolness + # text/plain always matches + mime = MIME("text/html") + + sprint(showerror, ex), mime end \ No newline at end of file diff --git a/src/webserver/NotebookServer.jl b/src/webserver/NotebookServer.jl index 70a8f5aa38..b89edaa475 100644 --- a/src/webserver/NotebookServer.jl +++ b/src/webserver/NotebookServer.jl @@ -167,10 +167,7 @@ function run(port = 1234, launchbrowser = false) if haskey(responses, messagetype) responsefunc = responses[messagetype] - response = responsefunc((client, body, args...)) - # if response !== nothing - # putplutoupdates!(response...) - # end + responsefunc((client, body, args...)) else @warn "Message of type $(messagetype) not recognised" end @@ -249,6 +246,9 @@ function run(port = 1234, launchbrowser = false) if isa(e, InterruptException) println("\nClosing Pluto... Bye! 🎈") close(serversocket) + for (uuid, ws) in WorkspaceManager.workspaces + WorkspaceManager.unmake_workspace(ws) + end else rethrow(e) end diff --git a/test/React.jl b/test/React.jl index 667022a997..796488a9f3 100644 --- a/test/React.jl +++ b/test/React.jl @@ -2,226 +2,256 @@ using Test using Pluto import Pluto: Notebook, Client, run_reactive!,fakeclient, createcell_fromcode, WorkspaceManager -@testset "Reactivity" begin +@testset "Reactivity $(method.name.name)" for method in [WorkspaceManager.ModuleWorkspace, WorkspaceManager.ProcessWorkspace] + WorkspaceManager.set_default_workspace_method(method) + + @test WorkspaceManager.default_workspace_method[] == method + fakeclient = Client(:fake, nothing) Pluto.connectedclients[fakeclient.id] = fakeclient @testset "Basic" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("x = 1"), - createcell_fromcode("y = x"), - createcell_fromcode("f(x) = x + y"), - createcell_fromcode("f(4)"), - ]) + createcell_fromcode("x = 1"), + createcell_fromcode("y = x"), + createcell_fromcode("f(x) = x + y"), + createcell_fromcode("f(4)"), + ]) fakeclient.connected_notebook = notebook + @test !haskey(WorkspaceManager.workspaces, notebook.uuid) + @test WorkspaceManager.get_workspace(notebook) isa method + run_reactive!(fakeclient, notebook, notebook.cells[1]) run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test notebook.cells[1].output == notebook.cells[2].output + @test notebook.cells[1].output_repr == notebook.cells[2].output_repr notebook.cells[1].code = "x = 12" run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test notebook.cells[1].output == notebook.cells[2].output + @test notebook.cells[1].output_repr == notebook.cells[2].output_repr run_reactive!(fakeclient, notebook, notebook.cells[3]) - @test notebook.cells[3].errormessage == nothing - + @test notebook.cells[3].error_repr == nothing + run_reactive!(fakeclient, notebook, notebook.cells[4]) - @test notebook.cells[4].output == 16 + @test notebook.cells[4].output_repr == "16" notebook.cells[1].code = "x = 912" run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test notebook.cells[4].output == 916 + @test notebook.cells[4].output_repr == "916" notebook.cells[3].code = "f(x) = x" run_reactive!(fakeclient, notebook, notebook.cells[3]) - @test notebook.cells[4].output == 4 + @test notebook.cells[4].output_repr == "4" + + WorkspaceManager.unmake_workspace(notebook) end - # https://github.com/fonsp/Pluto.jl/issues/32 +# https://github.com/fonsp/Pluto.jl/issues/32 @testset "Bad code" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("a"), - createcell_fromcode("1 = 2") - ]) + createcell_fromcode("a"), + createcell_fromcode("1 = 2") + ]) fakeclient.connected_notebook = notebook @test_nowarn run_reactive!(fakeclient, notebook, notebook.cells[1]) @test_nowarn run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test notebook.cells[1].errormessage !== nothing - @test notebook.cells[2].errormessage !== nothing + @test notebook.cells[1].error_repr !== nothing + @test notebook.cells[2].error_repr !== nothing + + WorkspaceManager.unmake_workspace(notebook) + end @testset "Mutliple assignments" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("x = 1"), - createcell_fromcode("x = 2"), - createcell_fromcode("f(x) = 3"), - createcell_fromcode("f(x) = 4"), - createcell_fromcode("g(x) = 5"), - createcell_fromcode("g = 6"), - ]) + createcell_fromcode("x = 1"), + createcell_fromcode("x = 2"), + createcell_fromcode("f(x) = 3"), + createcell_fromcode("f(x) = 4"), + createcell_fromcode("g(x) = 5"), + createcell_fromcode("g = 6"), + ]) fakeclient.connected_notebook = notebook - + run_reactive!(fakeclient, notebook, notebook.cells[1]) run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test occursin("Multiple", notebook.cells[1].errormessage) - @test occursin("Multiple", notebook.cells[2].errormessage) - + @test occursin("Multiple", notebook.cells[1].error_repr) + @test occursin("Multiple", notebook.cells[2].error_repr) + notebook.cells[1].code = "" run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test notebook.cells[1].errormessage == nothing - @test notebook.cells[2].errormessage == nothing - - # https://github.com/fonsp/Pluto.jl/issues/26 + @test notebook.cells[1].error_repr == nothing + @test notebook.cells[2].error_repr == nothing + + # https://github.com/fonsp/Pluto.jl/issues/26 notebook.cells[1].code = "x = 1" run_reactive!(fakeclient, notebook, notebook.cells[1]) notebook.cells[2].code = "x" run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test notebook.cells[1].errormessage == nothing - @test notebook.cells[2].errormessage == nothing + @test notebook.cells[1].error_repr == nothing + @test notebook.cells[2].error_repr == nothing run_reactive!(fakeclient, notebook, notebook.cells[3]) run_reactive!(fakeclient, notebook, notebook.cells[4]) - @test occursin("Multiple", notebook.cells[3].errormessage) - @test occursin("Multiple", notebook.cells[4].errormessage) - + @test occursin("Multiple", notebook.cells[3].error_repr) + @test occursin("Multiple", notebook.cells[4].error_repr) + notebook.cells[3].code = "" run_reactive!(fakeclient, notebook, notebook.cells[3]) - @test notebook.cells[3].errormessage == nothing - @test notebook.cells[4].errormessage == nothing - + @test notebook.cells[3].error_repr == nothing + @test notebook.cells[4].error_repr == nothing + run_reactive!(fakeclient, notebook, notebook.cells[5]) run_reactive!(fakeclient, notebook, notebook.cells[6]) - @test occursin("Multiple", notebook.cells[5].errormessage) - @test occursin("Multiple", notebook.cells[6].errormessage) - + @test occursin("Multiple", notebook.cells[5].error_repr) + @test occursin("Multiple", notebook.cells[6].error_repr) + notebook.cells[5].code = "" run_reactive!(fakeclient, notebook, notebook.cells[5]) - @test notebook.cells[5].errormessage == nothing - # @test_broken !occursin("redefinition of constant", notebook.cells[6].errormessage) + @test notebook.cells[5].error_repr == nothing + # @test_broken !occursin("redefinition of constant", notebook.cells[6].error_repr) + + WorkspaceManager.unmake_workspace(notebook) + end - - @testset "Cyclic" begin + + @testset "Circular" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("x = y"), - createcell_fromcode("y = x") - ]) + createcell_fromcode("x = y"), + createcell_fromcode("y = x") + ]) fakeclient.connected_notebook = notebook run_reactive!(fakeclient, notebook, notebook.cells[1]) run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test occursin("Cyclic reference", notebook.cells[1].errormessage) - @test occursin("Cyclic reference", notebook.cells[2].errormessage) + @test occursin("Circular reference", notebook.cells[1].error_repr) + @test occursin("Circular reference", notebook.cells[2].error_repr) + + WorkspaceManager.unmake_workspace(notebook) + end @testset "Variable deletion" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("x = 1"), - createcell_fromcode("y = x") - ]) + createcell_fromcode("x = 1"), + createcell_fromcode("y = x") + ]) fakeclient.connected_notebook = notebook run_reactive!(fakeclient, notebook, notebook.cells[1]) run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test notebook.cells[1].output == notebook.cells[2].output + @test notebook.cells[1].output_repr == notebook.cells[2].output_repr notebook.cells[1].code = "" run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test notebook.cells[1].output == nothing - @test notebook.cells[1].errormessage == nothing - @test notebook.cells[2].output == nothing - @test occursin("x not defined", notebook.cells[2].errormessage) + @test notebook.cells[1].output_repr == "" + @test notebook.cells[1].error_repr == nothing + @test notebook.cells[2].output_repr == nothing + @test occursin("x not defined", notebook.cells[2].error_repr) + + WorkspaceManager.unmake_workspace(notebook) + end @testset "Recursive function is not considered cyclic" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("f(n) = n * f(n-1)"), - createcell_fromcode("g(n) = h(n-1)"), - createcell_fromcode("h(n) = g(n-1)"), - ]) + createcell_fromcode("f(n) = n * f(n-1)"), + createcell_fromcode("g(n) = h(n-1)"), + createcell_fromcode("h(n) = g(n-1)"), + ]) fakeclient.connected_notebook = notebook run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test !isempty(methods(notebook.cells[1].output)) - @test notebook.cells[1].errormessage == nothing + @test startswith(notebook.cells[1].output_repr, "f (generic function with ") + @test notebook.cells[1].error_repr == nothing run_reactive!(fakeclient, notebook, notebook.cells[2]) run_reactive!(fakeclient, notebook, notebook.cells[3]) - @test notebook.cells[2].errormessage == nothing - @test notebook.cells[3].errormessage == nothing + @test notebook.cells[2].error_repr == nothing + @test notebook.cells[3].error_repr == nothing + + WorkspaceManager.unmake_workspace(notebook) + end @testset "Variable cannot reference its previous value" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("x = 3") - ]) + createcell_fromcode("x = 3") + ]) fakeclient.connected_notebook = notebook run_reactive!(fakeclient, notebook, notebook.cells[1]) notebook.cells[1].code = "x = x + 1" run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test notebook.cells[1].output == nothing - @test occursin("UndefVarError", notebook.cells[1].errormessage) + @test notebook.cells[1].output_repr == nothing + @test occursin("UndefVarError", notebook.cells[1].error_repr) + + WorkspaceManager.unmake_workspace(notebook) + end @testset "Changing functions" begin notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("y = 1"), - createcell_fromcode("f(x) = x + y"), - createcell_fromcode("f(3)"), + createcell_fromcode("y = 1"), + createcell_fromcode("f(x) = x + y"), + createcell_fromcode("f(3)"), - createcell_fromcode("g(a,b) = a+b"), - createcell_fromcode("g(5,6)"), + createcell_fromcode("g(a,b) = a+b"), + createcell_fromcode("g(5,6)"), - createcell_fromcode("h(x::Int64) = x"), - createcell_fromcode("h(7)"), - createcell_fromcode("h(8.0)"), - ]) + createcell_fromcode("h(x::Int64) = x"), + createcell_fromcode("h(7)"), + createcell_fromcode("h(8.0)"), + ]) fakeclient.connected_notebook = notebook run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test notebook.cells[2].errormessage == nothing + @test notebook.cells[2].error_repr == nothing run_reactive!(fakeclient, notebook, notebook.cells[1]) run_reactive!(fakeclient, notebook, notebook.cells[3]) - @test notebook.cells[3].output == 4 + @test notebook.cells[3].output_repr == "4" notebook.cells[1].code = "y = 2" run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test notebook.cells[3].output == 5 - @test notebook.cells[2].errormessage == nothing + @test notebook.cells[3].output_repr == "5" + @test notebook.cells[2].error_repr == nothing notebook.cells[1].code = "y" run_reactive!(fakeclient, notebook, notebook.cells[1]) - @test occursin("UndefVarError", notebook.cells[1].errormessage) - @test notebook.cells[2].errormessage == nothing - @test occursin("UndefVarError", notebook.cells[3].errormessage) + @test occursin("UndefVarError", notebook.cells[1].error_repr) + @test notebook.cells[2].error_repr == nothing + @test occursin("UndefVarError", notebook.cells[3].error_repr) run_reactive!(fakeclient, notebook, notebook.cells[4]) run_reactive!(fakeclient, notebook, notebook.cells[5]) - @test notebook.cells[5].output == 11 + @test notebook.cells[5].output_repr == "11" notebook.cells[4].code = "g(a) = a+a" run_reactive!(fakeclient, notebook, notebook.cells[4]) - @test notebook.cells[4].errormessage == nothing - @test notebook.cells[5].errormessage != nothing + @test notebook.cells[4].error_repr == nothing + @test notebook.cells[5].error_repr != nothing notebook.cells[5].code = "g(5)" run_reactive!(fakeclient, notebook, notebook.cells[5]) - @test notebook.cells[5].output == 10 + @test notebook.cells[5].output_repr == "10" run_reactive!(fakeclient, notebook, notebook.cells[6]) run_reactive!(fakeclient, notebook, notebook.cells[7]) run_reactive!(fakeclient, notebook, notebook.cells[8]) - @test notebook.cells[6].errormessage == nothing - @test notebook.cells[7].errormessage == nothing - @test notebook.cells[8].errormessage != nothing - + @test notebook.cells[6].error_repr == nothing + @test notebook.cells[7].error_repr == nothing + @test notebook.cells[8].error_repr != nothing + notebook.cells[6].code = "h(x::Float64) = 2.0 * x" run_reactive!(fakeclient, notebook, notebook.cells[6]) - @test notebook.cells[6].errormessage == nothing - @test notebook.cells[7].errormessage != nothing - @test notebook.cells[8].errormessage == nothing + @test notebook.cells[6].error_repr == nothing + @test notebook.cells[7].error_repr != nothing + @test notebook.cells[8].error_repr == nothing + + WorkspaceManager.unmake_workspace(notebook) + end # @testset "Multiple dispatch" begin @@ -254,60 +284,63 @@ import Pluto: Notebook, Client, run_reactive!,fakeclient, createcell_fromcode, # run_reactive!(fakeclient, notebook, notebook.cells[1]) # notebook.cells[1].code = "x = x + 1" # run_reactive!(fakeclient, notebook, notebook.cells[1]) -# @test notebook.cells[1].output == nothing -# @test occursin("UndefVarError", notebook.cells[1].errormessage) +# @test notebook.cells[1].output_repr == nothing +# @test occursin("UndefVarError", notebook.cells[1].error_repr) # end @testset "Immutable globals" begin - # We currently have a slightly relaxed version of immutable globals: - # globals can only be mutated/assigned _in a single cell_. + # We currently have a slightly relaxed version of immutable globals: + # globals can only be mutated/assigned _in a single cell_. notebook = Notebook(joinpath(tempdir(), "test.jl"), [ - createcell_fromcode("x = 1"), - createcell_fromcode("x = 2"), - createcell_fromcode("y = -3; y = 3"), - createcell_fromcode("z = 4"), - createcell_fromcode("let global z = 5 end"), - createcell_fromcode("w"), - createcell_fromcode("function f(x) global w = x end"), - createcell_fromcode("f(-8); f(8)"), - createcell_fromcode("f(9)"), - ]) + createcell_fromcode("x = 1"), + createcell_fromcode("x = 2"), + createcell_fromcode("y = -3; y = 3"), + createcell_fromcode("z = 4"), + createcell_fromcode("let global z = 5 end"), + createcell_fromcode("w"), + createcell_fromcode("function f(x) global w = x end"), + createcell_fromcode("f(-8); f(8)"), + createcell_fromcode("f(9)"), + ]) fakeclient.connected_notebook = notebook run_reactive!(fakeclient, notebook, notebook.cells[1]) run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test notebook.cells[1].output == nothing - @test notebook.cells[2].output == nothing - @test occursin("Multiple definitions for x", notebook.cells[1].errormessage) - @test occursin("Multiple definitions for x", notebook.cells[1].errormessage) - + @test notebook.cells[1].output_repr == nothing + @test notebook.cells[2].output_repr == nothing + @test occursin("Multiple definitions for x", notebook.cells[1].error_repr) + @test occursin("Multiple definitions for x", notebook.cells[1].error_repr) + notebook.cells[2].code = "x + 1" run_reactive!(fakeclient, notebook, notebook.cells[2]) - @test notebook.cells[1].output == 1 - @test notebook.cells[2].output == 2 - + @test notebook.cells[1].output_repr == "1" + @test notebook.cells[2].output_repr == "2" + run_reactive!(fakeclient, notebook, notebook.cells[3]) - @test notebook.cells[3].output == 3 + @test notebook.cells[3].output_repr == "3" run_reactive!(fakeclient, notebook, notebook.cells[4]) run_reactive!(fakeclient, notebook, notebook.cells[5]) - @test occursin("Multiple definitions for z", notebook.cells[4].errormessage) - @test occursin("Multiple definitions for z", notebook.cells[5].errormessage) - + @test occursin("Multiple definitions for z", notebook.cells[4].error_repr) + @test occursin("Multiple definitions for z", notebook.cells[5].error_repr) + run_reactive!(fakeclient, notebook, notebook.cells[6]) run_reactive!(fakeclient, notebook, notebook.cells[7]) - @test occursin("UndefVarError", notebook.cells[6].errormessage) - + @test occursin("UndefVarError", notebook.cells[6].error_repr) + run_reactive!(fakeclient, notebook, notebook.cells[8]) - @test notebook.cells[6].errormessage == nothing - @test notebook.cells[7].errormessage == nothing - @test notebook.cells[8].errormessage == nothing + @test notebook.cells[6].error_repr == nothing + @test notebook.cells[7].error_repr == nothing + @test notebook.cells[8].error_repr == nothing run_reactive!(fakeclient, notebook, notebook.cells[9]) - @test occursin("UndefVarError", notebook.cells[6].errormessage) - @test notebook.cells[7].errormessage == nothing - @test occursin("Multiple definitions for w", notebook.cells[8].errormessage) - @test occursin("Multiple definitions for w", notebook.cells[9].errormessage) + @test occursin("UndefVarError", notebook.cells[6].error_repr) + @test notebook.cells[7].error_repr == nothing + @test occursin("Multiple definitions for w", notebook.cells[8].error_repr) + @test occursin("Multiple definitions for w", notebook.cells[9].error_repr) + + WorkspaceManager.unmake_workspace(notebook) + end end \ No newline at end of file diff --git a/test/WorkspaceManager.jl b/test/WorkspaceManager.jl index d82f9b2e27..86bf364918 100644 --- a/test/WorkspaceManager.jl +++ b/test/WorkspaceManager.jl @@ -21,6 +21,6 @@ @test_nowarn run_reactive!(fakeclientA, notebookA, notebookA.cells[1]) @test_nowarn run_reactive!(fakeclientB, notebookB, notebookB.cells[1]) - @test notebookB.cells[1].errormessage !== nothing + @test notebookB.cells[1].error_repr !== nothing end end \ No newline at end of file