diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml index 69e7d8b5a5..06f747e7fd 100644 --- a/.github/workflows/IntegrationTest.yml +++ b/.github/workflows/IntegrationTest.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - julia-version: [1] + julia-version: ["1"] os: [ubuntu-latest] package: - { user: JuliaPluto, repo: PlutoSliderServer.jl } diff --git a/Project.toml b/Project.toml index 0ee3af8948..7612888fd5 100644 --- a/Project.toml +++ b/Project.toml @@ -22,6 +22,7 @@ Malt = "36869731-bdee-424d-aa32-cab38c994e3b" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" MsgPack = "99f44e22-a591-53d1-9472-aa23ef4bd671" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +PlutoDependencyExplorer = "72656b73-756c-7461-726b-72656b6b696b" PrecompileSignatures = "91cefc8d-f054-46dc-8f8c-26e11d7c5411" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" @@ -37,7 +38,7 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [compat] Base64 = "1" Configurations = "0.15, 0.16, 0.17" -Dates = "1" +Dates = "0, 1" Downloads = "1" ExpressionExplorer = "0.5, 0.6, 1" FileWatching = "1" @@ -52,6 +53,7 @@ Malt = "1.1" Markdown = "1" MsgPack = "1.1" Pkg = "1" +PlutoDependencyExplorer = "~1.0" PrecompileSignatures = "3" PrecompileTools = "1" REPL = "1" diff --git a/src/Pluto.jl b/src/Pluto.jl index d7f24b097d..5054456c3a 100644 --- a/src/Pluto.jl +++ b/src/Pluto.jl @@ -40,9 +40,7 @@ const PLUTO_VERSION = VersionNumber(Pkg.TOML.parsefile(joinpath(ROOT_DIR, "Proje const PLUTO_VERSION_STR = "v$(string(PLUTO_VERSION))" const JULIA_VERSION_STR = "v$(string(VERSION))" -include("./analysis/PlutoDependencyExplorer.jl") - -import .PlutoDependencyExplorer: TopologicalOrder, NotebookTopology, ExprAnalysisCache, ImmutableVector, ExpressionExplorerExtras, topological_order, all_cells, disjoint, where_assigned, where_referenced +import PlutoDependencyExplorer: PlutoDependencyExplorer, TopologicalOrder, NotebookTopology, ExprAnalysisCache, ImmutableVector, ExpressionExplorerExtras, topological_order, all_cells, disjoint, where_assigned, where_referenced using ExpressionExplorer include("./notebook/path helpers.jl") diff --git a/src/analysis/PlutoDependencyExplorer.jl b/src/analysis/PlutoDependencyExplorer.jl deleted file mode 100644 index 667bd860b2..0000000000 --- a/src/analysis/PlutoDependencyExplorer.jl +++ /dev/null @@ -1,22 +0,0 @@ -module PlutoDependencyExplorer - -using ExpressionExplorer - -""" -The `AbstractCell` type is the "unit of reactivity". It is used only as an indexing type in PlutoDependencyExplorer, its fields are not used. - -For example, the struct `Cycle <: ChildExplorationResult` stores a list of cells that reference each other in a cycle. This list is stored as a `Vector{<:AbstractCell}`. - -Pluto's `Cell` struct is a subtype of `AbstractCell`. So for example, the `Cycle` stores a `Vector{Cell}` when used in Pluto. -""" -abstract type AbstractCell end - -include("./PlutoDependencyExplorer/data structures.jl") -include("./PlutoDependencyExplorer/ExpressionExplorer.jl") -include("./PlutoDependencyExplorer/Topology.jl") -include("./PlutoDependencyExplorer/Errors.jl") -include("./PlutoDependencyExplorer/TopologicalOrder.jl") -include("./PlutoDependencyExplorer/topological_order.jl") -include("./PlutoDependencyExplorer/TopologyUpdate.jl") - -end \ No newline at end of file diff --git a/src/analysis/PlutoDependencyExplorer/Errors.jl b/src/analysis/PlutoDependencyExplorer/Errors.jl deleted file mode 100644 index 7c9af0bf30..0000000000 --- a/src/analysis/PlutoDependencyExplorer/Errors.jl +++ /dev/null @@ -1,39 +0,0 @@ -import Base: showerror -import ExpressionExplorer: FunctionName - -abstract type ReactivityError <: Exception end - -struct CyclicReferenceError <: ReactivityError - syms::Set{Symbol} -end - -function CyclicReferenceError(topology::NotebookTopology, cycle::AbstractVector{<:AbstractCell}) - CyclicReferenceError(cyclic_variables(topology, cycle)) -end - -struct MultipleDefinitionsError <: ReactivityError - syms::Set{Symbol} -end - -function MultipleDefinitionsError(topology::NotebookTopology, cell::AbstractCell, all_definers) - competitors = setdiff(all_definers, [cell]) - defs(c) = topology.nodes[c].funcdefs_without_signatures ∪ topology.nodes[c].definitions - MultipleDefinitionsError( - union((defs(cell) ∩ defs(c) for c in competitors)...) - ) -end - -const hint1 = "Combine all definitions into a single reactive cell using a `begin ... end` block." - -# TODO: handle case when cells are in cycle, but variables aren't -function showerror(io::IO, cre::CyclicReferenceError) - print(io, "Cyclic references among ") - println(io, join(cre.syms, ", ", " and ")) - print(io, hint1) -end - -function showerror(io::IO, mde::MultipleDefinitionsError) - print(io, "Multiple definitions for ") - println(io, join(mde.syms, ", ", " and ")) - print(io, hint1) # TODO: hint about mutable globals -end diff --git a/src/analysis/PlutoDependencyExplorer/ExpressionExplorer.jl b/src/analysis/PlutoDependencyExplorer/ExpressionExplorer.jl deleted file mode 100644 index 8d7e87f00c..0000000000 --- a/src/analysis/PlutoDependencyExplorer/ExpressionExplorer.jl +++ /dev/null @@ -1,234 +0,0 @@ -using ExpressionExplorer - -@deprecate ReactiveNode_from_expr(args...; kwargs...) ExpressionExplorer.compute_reactive_node(args...; kwargs...) - -module ExpressionExplorerExtras -import ..PlutoDependencyExplorer -using ExpressionExplorer -using ExpressionExplorer: ScopeState - -module Fake - module PlutoRunner - using Markdown - using InteractiveUtils - macro bind(def, element) - quote - global $(esc(def)) = element - end - end - end - import .PlutoRunner -end -import .Fake - - -""" -ExpressionExplorer does not explore inside macro calls, i.e. the arguments of a macrocall (like `a+b` in `@time a+b`) are ignored. -Normally, you would macroexpand an expression before giving it to ExpressionExplorer, but in Pluto we sometimes need to explore expressions *before* executing code. - -In those cases, we want most accurate result possible. Our extra needs are: -1. Macros included in Julia base, Markdown and `@bind` can be expanded statically. (See `maybe_macroexpand_pluto`.) -2. If a macrocall argument contains a "special heuristic" like `Pkg.activate()` or `using Something`, we need to surface this to be visible to ExpressionExplorer and Pluto. We do this by placing the macrocall in a block, and copying the argument after to the macrocall. -3. If a macrocall argument contains other macrocalls, we need these nested macrocalls to be visible. We do this by placing the macrocall in a block, and creating new macrocall expressions with the nested macrocall names, but without arguments. -""" -function pretransform_pluto(ex) - if Meta.isexpr(ex, :macrocall) - to_add = Expr[] - - maybe_expanded = maybe_macroexpand_pluto(ex) - if maybe_expanded === ex - # we were not able to expand statically - for arg in ex.args[begin+1:end] - arg_transformed = pretransform_pluto(arg) - macro_arg_symstate = ExpressionExplorer.compute_symbols_state(arg_transformed) - - # When this macro has something special inside like `Pkg.activate()`, we're going to make sure that ExpressionExplorer treats it as normal code, not inside a macrocall. (so these heuristics trigger later) - if arg isa Expr && macro_has_special_heuristic_inside(symstate = macro_arg_symstate, expr = arg_transformed) - # then the whole argument expression should be added - push!(to_add, arg_transformed) - else - for fn in macro_arg_symstate.macrocalls - push!(to_add, Expr(:macrocall, fn)) - # fn is a FunctionName - # normally this would not be a legal expression, but ExpressionExplorer handles it correctly so it's all cool - end - end - end - - Expr( - :block, - # the original expression, not expanded. ExpressionExplorer will just explore the name of the macro, and nothing else. - ex, - # any expressions that we need to sneakily add - to_add... - ) - else - Expr( - :block, - # We were able to expand the macro, so let's recurse on the result. - pretransform_pluto(maybe_expanded), - # the name of the macro that got expanded - Expr(:macrocall, ex.args[1]), - ) - end - elseif Meta.isexpr(ex, :module) - ex - elseif ex isa Expr - # recurse - Expr(ex.head, (pretransform_pluto(a) for a in ex.args)...) - else - ex - end -end - - - -""" -Uses `cell_precedence_heuristic` to determine if we need to include the contents of this macro in the symstate. -This helps with things like a Pkg.activate() that's in a macro, so Pluto still understands to disable nbpkg. -""" -function macro_has_special_heuristic_inside(; symstate::SymbolsState, expr::Expr)::Bool - # Also, because I'm lazy and don't want to copy any code, imma use cell_precedence_heuristic here. - # Sad part is, that this will also include other symbols used in this macro... but come'on - node = ReactiveNode(symstate) - code = PlutoDependencyExplorer.ExprAnalysisCache( - parsedcode = expr, - module_usings_imports = ExpressionExplorer.compute_usings_imports(expr), - ) - - return PlutoDependencyExplorer.cell_precedence_heuristic(node, code) < PlutoDependencyExplorer.DEFAULT_PRECEDENCE_HEURISTIC -end - -const can_macroexpand_no_bind = Set(Symbol.(["@md_str", "Markdown.@md_str", "@gensym", "Base.@gensym", "@enum", "Base.@enum", "@assert", "Base.@assert", "@cmd", "Base.@cmd", "@doc", "Base.@doc", "Core.@doc"])) -const can_macroexpand = can_macroexpand_no_bind ∪ Set(Symbol.(["@bind", "PlutoRunner.@bind"])) - -const plutorunner_id = Base.PkgId(Base.UUID("dc6b355a-2368-4481-ae6d-ae0351418d79"), "PlutoRunner") -const pluto_id = Base.PkgId(Base.UUID("c3e4b0f8-55cb-11ea-2926-15256bba5781"), "Pluto") -const found_plutorunner = Ref{Union{Nothing,Module}}(nothing) - -""" -Find the module `PlutoRunner`, if it is currently loaded. We use `PlutoRunner` to macroexpand `@bind`. If not found, the fallback is `Fake.PlutoRunner`. -""" -function get_plutorunner() - fpr = found_plutorunner[] - if fpr === nothing - # lets try really hard to find it! - if haskey(Base.loaded_modules, pluto_id) - found_plutorunner[] = Base.loaded_modules[pluto_id].PlutoRunner - elseif haskey(Base.loaded_modules, plutorunner_id) - found_plutorunner[] = Base.loaded_modules[plutorunner_id] - elseif isdefined(Main, :PlutoRunner) && Main.PlutoRunner isa Module - found_plutorunner[] = Main.PlutoRunner - else - # not found - Fake.PlutoRunner - end - else - fpr - end -end - -""" -If the macro is **known to Pluto**, expand or 'mock expand' it, if not, return the expression. Macros from external packages are not expanded, this is done later in the pipeline. See https://github.com/fonsp/Pluto.jl/pull/1032 -""" -function maybe_macroexpand_pluto(ex::Expr; recursive::Bool=false, expand_bind::Bool=true) - result::Expr = if ex.head === :macrocall - funcname = ExpressionExplorer.split_funcname(ex.args[1]) - - if funcname.joined ∈ (expand_bind ? can_macroexpand : can_macroexpand_no_bind) - try - macroexpand(get_plutorunner(), ex; recursive=false)::Expr - catch e - @debug "Could not macroexpand" ex exception=(e, catch_backtrace()) - ex - end - else - ex - end - else - ex - end - - if recursive - # Not using broadcasting because that is expensive compilation-wise for `result.args::Any`. - expanded = Any[] - for arg in result.args - ex = maybe_macroexpand_pluto(arg; recursive, expand_bind) - push!(expanded, ex) - end - return Expr(result.head, expanded...) - else - return result - end -end - -maybe_macroexpand_pluto(ex::Any; kwargs...) = ex - - - -############### - -function collect_implicit_usings(usings_imports::ExpressionExplorer.UsingsImports) - implicit_usings = Set{Expr}() - for (using_, isglobal) in zip(usings_imports.usings, usings_imports.usings_isglobal) - if !(isglobal && is_implicit_using(using_)) - continue - end - - for arg in using_.args - push!(implicit_usings, transform_dot_notation(arg)) - end - end - implicit_usings -end - -is_implicit_using(ex::Expr) = Meta.isexpr(ex, :using) && length(ex.args) >= 1 && !Meta.isexpr(ex.args[1], :(:)) - -function transform_dot_notation(ex::Expr) - if Meta.isexpr(ex, :(.)) - Expr(:block, ex.args[end]) - else - ex - end -end - - - -############### - - -""" -```julia -can_be_function_wrapped(ex)::Bool -``` - -Is this code simple enough that we can wrap it inside a function, and run the function in global scope instead of running the code directly? Look for `Pluto.PlutoRunner.Computer` to learn more. -""" -function can_be_function_wrapped(x::Expr) - if x.head === :global || # better safe than sorry - x.head === :using || - x.head === :import || - x.head === :export || - x.head === :public || # Julia 1.11 - x.head === :module || - x.head === :incomplete || - # Only bail on named functions, but anonymous functions (args[1].head == :tuple) are fine. - # TODO Named functions INSIDE other functions should be fine too - (x.head === :function && !Meta.isexpr(x.args[1], :tuple)) || - x.head === :macro || - # Cells containing macrocalls will actually be function wrapped using the expanded version of the expression - # See https://github.com/fonsp/Pluto.jl/pull/1597 - x.head === :macrocall || - x.head === :struct || - x.head === :abstract || - (x.head === :(=) && ExpressionExplorer.is_function_assignment(x)) || # f(x) = ... - (x.head === :call && (x.args[1] === :eval || x.args[1] === :include)) - false - else - all(can_be_function_wrapped, x.args) - end -end - -can_be_function_wrapped(x::Any) = true - -end diff --git a/src/analysis/PlutoDependencyExplorer/TopologicalOrder.jl b/src/analysis/PlutoDependencyExplorer/TopologicalOrder.jl deleted file mode 100644 index 085fdc0273..0000000000 --- a/src/analysis/PlutoDependencyExplorer/TopologicalOrder.jl +++ /dev/null @@ -1,10 +0,0 @@ -import ExpressionExplorer: SymbolsState, FunctionName - -"Information container about the cells to run in a reactive call and any cells that will err." -Base.@kwdef struct TopologicalOrder{C <: AbstractCell} - input_topology::NotebookTopology - "Cells that form a directed acyclic graph, in topological order." - runnable::Vector{C} - "Cells that are in a directed cycle, with corresponding `ReactivityError`s." - errable::Dict{C,ReactivityError} -end diff --git a/src/analysis/PlutoDependencyExplorer/Topology.jl b/src/analysis/PlutoDependencyExplorer/Topology.jl deleted file mode 100644 index 766e7516e4..0000000000 --- a/src/analysis/PlutoDependencyExplorer/Topology.jl +++ /dev/null @@ -1,74 +0,0 @@ -import ExpressionExplorer: UsingsImports, SymbolsState - -"A container for the result of parsing the cell code, with some extra metadata." -Base.@kwdef struct ExprAnalysisCache - code::String="" - parsedcode::Expr=Expr(:toplevel, LineNumberNode(1), Expr(:block)) - module_usings_imports::UsingsImports = UsingsImports() - function_wrapped::Bool=false - forced_expr_id::Union{UInt,Nothing}=nothing -end - -function ExprAnalysisCache(code_str::String, parsedcode::Expr) - ExprAnalysisCache(; - code=code_str, - parsedcode, - module_usings_imports=ExpressionExplorer.compute_usings_imports(parsedcode), - function_wrapped=ExpressionExplorerExtras.can_be_function_wrapped(parsedcode), - ) -end - -function ExprAnalysisCache(old_cache::ExprAnalysisCache; new_properties...) - properties = Dict{Symbol,Any}(field => getproperty(old_cache, field) for field in fieldnames(ExprAnalysisCache)) - merge!(properties, Dict{Symbol,Any}(new_properties)) - ExprAnalysisCache(;properties...) -end - -"The (information needed to create the) dependency graph of a notebook. Cells are linked by the names of globals that they define and reference. 🕸" -Base.@kwdef struct NotebookTopology{C <: AbstractCell} - nodes::ImmutableDefaultDict{C,ReactiveNode}=ImmutableDefaultDict{C,ReactiveNode}(ReactiveNode) - codes::ImmutableDefaultDict{C,ExprAnalysisCache}=ImmutableDefaultDict{C,ExprAnalysisCache}(ExprAnalysisCache) - cell_order::ImmutableVector{C}=ImmutableVector{C}() - - unresolved_cells::ImmutableSet{C} = ImmutableSet{C}() - disabled_cells::ImmutableSet{C} = ImmutableSet{C}() -end - -# BIG TODO HERE: CELL ORDER -all_cells(topology::NotebookTopology) = topology.cell_order.c - -is_resolved(topology::NotebookTopology) = isempty(topology.unresolved_cells) -is_resolved(topology::NotebookTopology, c::AbstractCell) = c in topology.unresolved_cells - -is_disabled(topology::NotebookTopology, c::AbstractCell) = c in topology.disabled_cells - -function set_unresolved(topology::NotebookTopology{C}, unresolved_cells::Vector{C}) where C <: AbstractCell - codes = Dict{C,ExprAnalysisCache}( - cell => ExprAnalysisCache(topology.codes[cell]; function_wrapped=false, forced_expr_id=nothing) - for cell in unresolved_cells - ) - NotebookTopology{C}( - nodes=topology.nodes, - codes=merge(topology.codes, codes), - unresolved_cells=union(topology.unresolved_cells, unresolved_cells), - cell_order=topology.cell_order, - disabled_cells=topology.disabled_cells, - ) -end - - -""" - exclude_roots(topology::NotebookTopology, roots_to_exclude)::NotebookTopology - -Returns a new topology as if `topology` was created with all code for `roots_to_exclude` -being empty, preserving disabled cells and cell order. -""" -function exclude_roots(topology::NotebookTopology{C}, cells::Vector{C}) where C <: AbstractCell - NotebookTopology{C}( - nodes=setdiffkeys(topology.nodes, cells), - codes=setdiffkeys(topology.codes, cells), - unresolved_cells=ImmutableSet{C}(setdiff(topology.unresolved_cells.c, cells); skip_copy=true), - cell_order=topology.cell_order, - disabled_cells=topology.disabled_cells, - ) -end diff --git a/src/analysis/PlutoDependencyExplorer/TopologyUpdate.jl b/src/analysis/PlutoDependencyExplorer/TopologyUpdate.jl deleted file mode 100644 index 24e70e6cbf..0000000000 --- a/src/analysis/PlutoDependencyExplorer/TopologyUpdate.jl +++ /dev/null @@ -1,115 +0,0 @@ -import ExpressionExplorer -import .ExpressionExplorerExtras -import ExpressionExplorer: SymbolsState, FunctionNameSignaturePair - - -""" -```julia -function updated_topology( - old_topology::NotebookTopology{C}, notebook_cells::Iterable{C}, updated_cells::Iterable{C}; - get_code_str::Function, - get_code_expr::Function, - get_cell_disabled::Function=c->false, -) where C <: AbstractCell -``` - -Return a copy of `old_topology`, but with new reactivity information from `updated_cells` taken into account. This function is used when cell code changes. - -`notebook_cells` should contain all cells in the reactive document. - -The functions `get_code_str` and `get_code_expr` should return the code string and parsed expression for a given cell. `get_cell_disabled` should return `true` if a cell is disabled, defaults to `false`. -""" -function updated_topology( - old_topology::NotebookTopology{C}, notebook_cells, updated_cells; - get_code_str::Function, - get_code_expr::Function, - get_cell_disabled::Function=c->false, -) where C <: AbstractCell - - updated_codes = Dict{C,ExprAnalysisCache}() - updated_nodes = Dict{C,ReactiveNode}() - - for cell in updated_cells - # TODO this needs to be extracted somehow - - old_code = old_topology.codes[cell] - new_code_str = get_code_str(cell) - - if old_code.code !== new_code_str - parsedcode = get_code_expr(cell) - new_code = updated_codes[cell] = ExprAnalysisCache(new_code_str, parsedcode) - new_reactive_node = ExpressionExplorer.compute_reactive_node(ExpressionExplorerExtras.pretransform_pluto(new_code.parsedcode)) - - updated_nodes[cell] = new_reactive_node - elseif old_code.forced_expr_id !== nothing - # reset computer code - updated_codes[cell] = ExprAnalysisCache(old_code; forced_expr_id=nothing, function_wrapped=false) - end - end - - - old_cells = all_cells(old_topology) - removed_cells = setdiff(old_cells, notebook_cells) - if isempty(removed_cells) - # We can keep identity - new_codes = merge(old_topology.codes, updated_codes) - new_nodes = merge(old_topology.nodes, updated_nodes) - else - new_codes = merge(setdiffkeys(old_topology.codes, removed_cells), updated_codes) - new_nodes = merge(setdiffkeys(old_topology.nodes, removed_cells), updated_nodes) - end - - new_unresolved_set = setdiff!( - union!( - Set{C}(), - # all cells that were unresolved before, and did not change code... - Iterators.filter(old_topology.unresolved_cells) do c - !haskey(updated_nodes, c) - end, - # ...plus all cells that changed, and now use a macrocall... - Iterators.filter(updated_cells) do c - !isempty(new_nodes[c].macrocalls) - end, - ), - # ...minus cells that were removed. - removed_cells, - ) - - new_disabled_set = setdiff!( - union!( - Set{C}(), - # all cells that were disabled before... - old_topology.disabled_cells, - # ...plus all cells that changed... - updated_cells, - ), - # ...minus cells that changed and are not disabled. - Iterators.filter(!get_cell_disabled, updated_cells), - ) - - unresolved_cells = if new_unresolved_set == old_topology.unresolved_cells - old_topology.unresolved_cells - else - ImmutableSet(new_unresolved_set; skip_copy=true) - end - - disabled_cells = if new_disabled_set == old_topology.disabled_cells - old_topology.disabled_cells - else - ImmutableSet(new_disabled_set; skip_copy=true) - end - - cell_order = if old_cells == notebook_cells - old_topology.cell_order - else - ImmutableVector(notebook_cells) # makes a copy - end - - NotebookTopology{C}(; - nodes=new_nodes, - codes=new_codes, - unresolved_cells, - disabled_cells, - cell_order, - ) -end diff --git a/src/analysis/PlutoDependencyExplorer/data structures.jl b/src/analysis/PlutoDependencyExplorer/data structures.jl deleted file mode 100644 index 0ecb292ef0..0000000000 --- a/src/analysis/PlutoDependencyExplorer/data structures.jl +++ /dev/null @@ -1,110 +0,0 @@ - -begin - """ - ```julia - ImmutableSet{T}(xs::Set{T}) - ``` - - Wraps around, and behaves like a regular `Set`, but mutating operations (like `push!` or `empty!`) are not allowed. - - When called on a set, a *shallow copy* of the set is stored. This means that it's fine to mutate the input set after creating an `ImmutableSet` from it. To prevent this, call `ImmutableSet(xs; skip_copy=true)`. - """ - struct ImmutableSet{T} <: AbstractSet{T} - c::Set{T} - ImmutableSet{T}(s::Set{T}; skip_copy::Bool=false) where T = new{T}(skip_copy ? s : copy(s)) - end - ImmutableSet(s::Set{T}; skip_copy::Bool=false) where T = ImmutableSet{T}(s; skip_copy) - - ImmutableSet(arg) = let - s = Set(arg) - ImmutableSet{eltype(s)}(s; skip_copy=true) - end - ImmutableSet{T}() where T = ImmutableSet{T}(Set{T}(); skip_copy=true) - ImmutableSet() = ImmutableSet(Set(); skip_copy=true) - - Base.copy(s::ImmutableSet) = ImmutableSet(copy(s.c)) - Base.in(x, s::ImmutableSet) = Base.in(x, s.c) - Base.isempty(s::ImmutableSet) = Base.isempty(s.c) - Base.length(s::ImmutableSet) = Base.length(s.c) - Base.iterate(s::ImmutableSet, i...) = Base.iterate(s.c, i...) - Base.setdiff(s::ImmutableSet, i...) = ImmutableSet(setdiff(s.c, i...)) -end - -begin - """ - ```julia - ImmutableVector{T}(xs::Vector{T}) - ``` - - Wraps around, and behaves like a regular `Vector`, but mutating operations (like `push!` or `setindex!`) are not allowed. - - When called on a vector, a *shallow copy* of the vector is stored. This means that it's fine to mutate the input vector after creating an `ImmutableVector` from it. To prevent this, call `ImmutableVector(xs; skip_copy=true)`. - """ - struct ImmutableVector{T} <: AbstractVector{T} - c::Vector{T} - ImmutableVector{T}(x; skip_copy::Bool=false) where T = new{T}(skip_copy ? x : copy(x)) - end - ImmutableVector(x::AbstractVector{T}; kwargs...) where T = ImmutableVector{T}(x; kwargs...) - ImmutableVector{T}() where T = ImmutableVector{T}(Vector{T}()) - - Base.copy(s::ImmutableVector) = ImmutableVector(copy(s.c)) - Base.in(x, s::ImmutableVector) = Base.in(x, s.c) - Base.isempty(s::ImmutableVector) = Base.isempty(s.c) - Base.length(s::ImmutableVector) = Base.length(s.c) - Base.size(s::ImmutableVector) = Base.size(s.c) - Base.iterate(s::ImmutableVector, i...) = Base.iterate(s.c, i...) - Base.getindex(s::ImmutableVector, i::Integer) = Base.getindex(s.c, i) - Base.getindex(s::ImmutableVector, i...) = ImmutableVector(Base.getindex(s.c, i...)) - delete_unsafe!(s::ImmutableSet, args...) = Base.delete!(s.c, args...) -end - -begin - """ - ```julia - ImmutableDefaultDict{K,V}(default::Function, container::Dict{K,V}) - ``` - - Wraps around, and behaves like a regular `Dict`, but if a key is not found, it will call return `default()`. - """ - struct ImmutableDefaultDict{K,V} <: AbstractDict{K,V} - default::Union{Function,DataType} - container::Dict{K,V} - end - - ImmutableDefaultDict{K,V}(default::Union{Function,DataType}) where {K,V} = ImmutableDefaultDict{K,V}(default, Dict{K,V}()) - - function Base.getindex(aid::ImmutableDefaultDict{K,V}, key::K)::V where {K,V} - get!(aid.default, aid.container, key) - end - function Base.merge(a1::ImmutableDefaultDict{K,V}, a2::ImmutableDefaultDict{K,V}) where {K,V} - isempty(a2) ? a1 : ImmutableDefaultDict{K,V}(a1.default, merge(a1.container, a2.container)) - end - function Base.merge(a1::ImmutableDefaultDict{K,V}, a2::AbstractDict) where {K,V} - isempty(a2) ? a1 : ImmutableDefaultDict{K,V}(a1.default, merge(a1.container, a2)) - end - # disabled because it's immutable! - # Base.setindex!(aid::ImmutableDefaultDict{K,V}, args...) where {K,V} = Base.setindex!(aid.container, args...) - # Base.delete!(aid::ImmutableDefaultDict{K,V}, args...) where {K,V} = Base.delete!(aid.container, args...) - delete_unsafe!(aid::ImmutableDefaultDict{K,V}, args...) where {K,V} = Base.delete!(aid.container, args...) - Base.copy(aid::ImmutableDefaultDict{K,V}) where {K,V} = ImmutableDefaultDict{K,V}(aid.default, copy(aid.container)) - Base.keys(aid::ImmutableDefaultDict) = Base.keys(aid.container) - Base.values(aid::ImmutableDefaultDict) = Base.values(aid.container) - Base.length(aid::ImmutableDefaultDict) = Base.length(aid.container) - Base.iterate(aid::ImmutableDefaultDict, args...) = Base.iterate(aid.container, args...) -end - -""" -```julia -setdiffkeys(d::Dict{K,V}, key_itrs...)::Dict{K,V} -``` - -Apply `setdiff` on the keys of a dictionary. - -# Example -```julia -setdiffkeys(Dict(1 => "one", 2 => "two", 3 => "three"), [1, 3]) -# result: `Dict(2 => "two")` -``` -""" -setdiffkeys(d::Dict{K,V}, key_itrs...) where {K,V} = Dict{K,V}(k => d[k] for k in setdiff(keys(d), key_itrs...)) -setdiffkeys(d::ImmutableDefaultDict{K,V}, key_itrs...) where {K,V} = ImmutableDefaultDict{K,V}(d.default, setdiffkeys(d.container, key_itrs...)) diff --git a/src/analysis/PlutoDependencyExplorer/topological_order.jl b/src/analysis/PlutoDependencyExplorer/topological_order.jl deleted file mode 100644 index fde60be434..0000000000 --- a/src/analysis/PlutoDependencyExplorer/topological_order.jl +++ /dev/null @@ -1,260 +0,0 @@ -abstract type ChildExplorationResult end - -struct Ok <: ChildExplorationResult end -struct Cycle{C <: AbstractCell} <: ChildExplorationResult - cycled_cells::Vector{C} -end - -""" -Return a `TopologicalOrder` that lists the cells to be evaluated in a single reactive run, in topological order. Includes the given roots. - -# Keyword arguments - -- `allow_multiple_defs::Bool = false` - - If `false` (default), multiple definitions are not allowed. When a cell is found that defines a variable that is also defined by another cell (this other cell is called a *fellow assigner*), then both cells are marked as `errable` and not `runnable`. - - If `true`, then multiple definitions are allowed, in the sense that we ignore the existance of other cells that define the same variable. - - -- `skip_at_partial_multiple_defs::Bool = false` - - If `true` (not default), and `allow_multiple_defs = true` (not default), then the search stops going downward when finding a cell that has fellow assigners, *unless all fellow assigners can be reached by the `roots`*, in which case we continue searching downward. - - In other words, if there is a set of fellow assigners that can only be reached **partially** by the roots, then this set blocks the search, and cells that depend on the set are not found. -""" -function topological_order(topology::NotebookTopology{C}, roots::AbstractVector{C}; - allow_multiple_defs::Bool=false, - skip_at_partial_multiple_defs::Bool=false, -)::TopologicalOrder{C} where C <: AbstractCell - - if skip_at_partial_multiple_defs - @assert allow_multiple_defs - end - - entries = C[] - exits = C[] - errable = Dict{C,ReactivityError}() - - # https://xkcd.com/2407/ - function bfs(cell::C)::ChildExplorationResult - if cell in exits - return Ok() - elseif haskey(errable, cell) - return Ok() - elseif length(entries) > 0 && entries[end] === cell - return Ok() # a cell referencing itself is legal - elseif cell in entries - currently_in = setdiff(entries, exits) - cycle = currently_in[findfirst(isequal(cell), currently_in):end] - - if !cycle_is_among_functions(topology, cycle) - for cell in cycle - errable[cell] = CyclicReferenceError(topology, cycle) - end - return Cycle(cycle) - end - - return Ok() - end - - # used for cleanups of wrong cycles - current_entries_num = length(entries) - current_exits_num = length(exits) - - push!(entries, cell) - - assigners = where_assigned(topology, cell) - referencers = where_referenced(topology, cell) |> Iterators.reverse - - if !allow_multiple_defs && length(assigners) > 1 - for c in assigners - errable[c] = MultipleDefinitionsError(topology, c, assigners) - end - end - - should_continue_search_down = !skip_at_partial_multiple_defs || all(c -> c === cell || c ∈ exits, assigners) - should_search_fellow_assigners_if_any = !allow_multiple_defs - - to_search_next = if should_continue_search_down - if should_search_fellow_assigners_if_any - union(assigners, referencers) - else - referencers - end - else - C[] - end - - for c in to_search_next - if c !== cell - child_result = bfs(c) - - # No cycle for this child or the cycle has no soft edges - if child_result isa Ok || cell ∉ child_result.cycled_cells - continue - end - - # Can we cleanup the cycle from here or is it caused by a parent cell? - # if the edge to the child cell is composed of soft assigments only then we can try to "break" - # it else we bubble the result up to the parent until it is - # either out of the cycle or a soft-edge is found - if !is_soft_edge(topology, cell, c) - # Cleanup all entries & child exits - deleteat!(entries, current_entries_num+1:length(entries)) - deleteat!(exits, current_exits_num+1:length(exits)) - return child_result - end - - # Cancel exploring this child (c) - # 1. Cleanup the errables - for cycled_cell in child_result.cycled_cells - delete!(errable, cycled_cell) - end - # 2. Remove the current child (c) from the entries if it was just added - if entries[end] === c - pop!(entries) - end - - continue # the cycle was created by us so we can keep exploring other children - end - end - push!(exits, cell) - Ok() - end - - # we first move cells to the front if they call `import` or `using` - # we use MergeSort because it is a stable sort: leaves cells in order if they are in the same category - prelim_order_1 = sort(roots, alg=MergeSort, by=c -> cell_precedence_heuristic(topology, c)) - # reversing because our search returns reversed order - for i in length(prelim_order_1):-1:1 - bfs(prelim_order_1[i]) - end - ordered = reverse(exits) - TopologicalOrder(topology, setdiff(ordered, keys(errable)), errable) -end - -topological_order(topology::NotebookTopology; kwargs...) = topological_order(topology, all_cells(topology); kwargs...) - -Base.collect(notebook_topo_order::TopologicalOrder) = union(notebook_topo_order.runnable, keys(notebook_topo_order.errable)) - -function disjoint(a, b) - !any(x in a for x in b) -end - -"Return the cells that reference any of the symbols defined by the given cell. Non-recursive: only direct dependencies are found." -function where_referenced(topology::NotebookTopology{C}, myself::C)::Vector{C} where C <: AbstractCell - to_compare = union(topology.nodes[myself].definitions, topology.nodes[myself].soft_definitions, topology.nodes[myself].funcdefs_without_signatures) - where_referenced(topology, to_compare) -end -"Return the cells that reference any of the given symbols. Non-recursive: only direct dependencies are found." -function where_referenced(topology::NotebookTopology{C}, to_compare::Set{Symbol})::Vector{C} where C <: AbstractCell - return filter(all_cells(topology)) do cell - !disjoint(to_compare, topology.nodes[cell].references) - end -end - -"Returns whether or not the edge between two cells is composed only of \"soft\"-definitions" -function is_soft_edge(topology::NotebookTopology{C}, parent_cell::C, child_cell::C) where C <: AbstractCell - hard_definitions = union(topology.nodes[parent_cell].definitions, topology.nodes[parent_cell].funcdefs_without_signatures) - soft_definitions = topology.nodes[parent_cell].soft_definitions - - child_references = topology.nodes[child_cell].references - - disjoint(hard_definitions, child_references) && !disjoint(soft_definitions, child_references) -end - - -"Return the cells that also assign to any variable or method defined by the given cell. If more than one cell is returned (besides the given cell), then all of them should throw a `MultipleDefinitionsError`. Non-recursive: only direct dependencies are found." -function where_assigned(topology::NotebookTopology{C}, myself::C)::Vector{C} where C - where_assigned(topology, topology.nodes[myself]) -end - -function where_assigned(topology::NotebookTopology{C}, self::ReactiveNode)::Vector{C} where C - return filter(all_cells(topology)) do cell - other = topology.nodes[cell] - !( - disjoint(self.definitions, other.definitions) && - - disjoint(self.definitions, other.funcdefs_without_signatures) && - disjoint(self.funcdefs_without_signatures, other.definitions) && - - disjoint(self.funcdefs_with_signatures, other.funcdefs_with_signatures) - ) - end -end - -function where_assigned(topology::NotebookTopology{C}, to_compare::Set{Symbol})::Vector{C} where C - filter(all_cells(topology)) do cell - other = topology.nodes[cell] - !( - disjoint(to_compare, other.definitions) && - disjoint(to_compare, other.funcdefs_without_signatures) - ) - end -end - - - -function cyclic_variables(topology::NotebookTopology, cycle)::Set{Symbol} - referenced_during_cycle = union!(Set{Symbol}(), (topology.nodes[c].references for c in cycle)...) - assigned_during_cycle = union!(Set{Symbol}(), (topology.nodes[c].definitions ∪ topology.nodes[c].soft_definitions ∪ topology.nodes[c].funcdefs_without_signatures for c in cycle)...) - - referenced_during_cycle ∩ assigned_during_cycle -end - -function cycle_is_among_functions(topology::NotebookTopology, cycle)::Bool - cyclics = cyclic_variables(topology, cycle) - - all( - any(s ∈ topology.nodes[c].funcdefs_without_signatures for c in cycle) - for s in cyclics - ) -end - -function cell_precedence_heuristic(topology::NotebookTopology{C}, cell::C) where C <: AbstractCell - cell_precedence_heuristic(topology.nodes[cell], topology.codes[cell]) -end - - -"""Assigns a number to a cell - cells with a lower number might run first. - -This is used to treat reactive dependencies between cells that cannot be found using static code anylsis.""" -function cell_precedence_heuristic(node::ReactiveNode, code::ExprAnalysisCache)::Real - if :Pkg ∈ node.definitions - 1 - elseif :DrWatson ∈ node.definitions - 2 - elseif Symbol("Pkg.API.activate") ∈ node.references || - Symbol("Pkg.activate") ∈ node.references || - Symbol("@pkg_str") ∈ node.references || - # https://juliadynamics.github.io/DrWatson.jl/dev/project/#DrWatson.quickactivate - Symbol("quickactivate") ∈ node.references || - Symbol("@quickactivate") ∈ node.references || - Symbol("DrWatson.@quickactivate") ∈ node.references || - Symbol("DrWatson.quickactivate") ∈ node.references - 3 - elseif Symbol("Pkg.API.add") ∈ node.references || - Symbol("Pkg.add") ∈ node.references || - Symbol("Pkg.API.develop") ∈ node.references || - Symbol("Pkg.develop") ∈ node.references - 4 - elseif :LOAD_PATH ∈ node.references - # https://github.com/fonsp/Pluto.jl/issues/323 - 5 - elseif :Revise ∈ node.definitions - # Load Revise before other packages so that it can properly `revise` them. - 6 - elseif !isempty(code.module_usings_imports.usings) - # always do `using X` before other cells, because we don't (yet) know which cells depend on it (we only know it with `import X` and `import X: y, z`) - 7 - elseif :include ∈ node.references - # https://github.com/fonsp/Pluto.jl/issues/193 - # because we don't (yet) know which cells depend on it - 8 - else - DEFAULT_PRECEDENCE_HEURISTIC - end -end - -const DEFAULT_PRECEDENCE_HEURISTIC = 9 diff --git a/test/Analysis.jl b/test/Analysis.jl deleted file mode 100644 index f9182ac30c..0000000000 --- a/test/Analysis.jl +++ /dev/null @@ -1,139 +0,0 @@ -using Test -import Pluto: Notebook, Cell, updated_topology, static_resolve_topology, is_just_text, NotebookTopology - -@testset "Analysis" begin - @testset "is_just_text" begin - notebook = Notebook([ - Cell(""), - Cell("md\"a\""), - Cell("html\"a\""), - Cell("md\"a \$b\$\""), - Cell("md\"a ``b``\""), - Cell(""" - let - x = md"a" - md"r \$x" - end - """), - Cell("html\"a 7 \$b\""), - - Cell("md\"a 8 \$b\""), - Cell("@a md\"asdf 9\""), - Cell("x()"), - Cell("x() = y()"), - Cell("12 + 12"), - Cell("import Dates"), - Cell("import Dates"), - Cell("while false end"), - Cell("for i in [16]; end"), - Cell("[i for i in [17]]"), - Cell("module x18 end"), - Cell(""" - module x19 - exit() - end - """), - Cell("""quote end"""), - Cell("""quote x = 21 end"""), - Cell("""quote \$(x = 22) end"""), - Cell("""asdf"23" """), - Cell("""@asdf("24") """), - Cell("""@x"""), - Cell("""@y z 26"""), - Cell("""f(g"27")"""), - ]) - - old = notebook.topology - new = notebook.topology = updated_topology(old, notebook, notebook.cells) - - @testset "Only-text detection" begin - @test is_just_text(new, notebook.cells[1]) - @test is_just_text(new, notebook.cells[2]) - @test is_just_text(new, notebook.cells[3]) - @test is_just_text(new, notebook.cells[4]) - @test is_just_text(new, notebook.cells[5]) - @test is_just_text(new, notebook.cells[6]) - @test is_just_text(new, notebook.cells[7]) - - @test !is_just_text(new, notebook.cells[8]) - @test !is_just_text(new, notebook.cells[9]) - @test !is_just_text(new, notebook.cells[10]) - @test !is_just_text(new, notebook.cells[11]) - @test !is_just_text(new, notebook.cells[12]) - @test !is_just_text(new, notebook.cells[13]) - @test !is_just_text(new, notebook.cells[14]) - @test !is_just_text(new, notebook.cells[15]) - @test !is_just_text(new, notebook.cells[16]) - @test !is_just_text(new, notebook.cells[17]) - @test !is_just_text(new, notebook.cells[18]) - @test !is_just_text(new, notebook.cells[19]) - @test !is_just_text(new, notebook.cells[20]) - @test !is_just_text(new, notebook.cells[21]) - @test !is_just_text(new, notebook.cells[22]) - @test !is_just_text(new, notebook.cells[23]) - @test !is_just_text(new, notebook.cells[24]) - @test !is_just_text(new, notebook.cells[25]) - @test !is_just_text(new, notebook.cells[26]) - @test !is_just_text(new, notebook.cells[27]) - end - end - - @testset "updated_topology identity" begin - notebook = Notebook([ - Cell("x = 1") - Cell("function f(x) - x + 1 - end") - Cell("a = x - 123") - Cell("") - Cell("") - Cell("") - ]) - - empty_top = notebook.topology - topo = updated_topology(empty_top, notebook, notebook.cells) - # updated_topology should preserve the identity of the topology if nothing changed. This means that we can cache the result of other functions in our code! - @test topo === updated_topology(topo, notebook, notebook.cells) - @test topo === updated_topology(topo, notebook, Cell[]) - @test topo === static_resolve_topology(topo) - - # for n in fieldnames(NotebookTopology) - # @test getfield(topo, n) === getfield(top2a, n) - # end - - setcode!(notebook.cells[1], "x = 999") - topo_2 = updated_topology(topo, notebook, notebook.cells[1:1]) - @test topo_2 !== topo - - - setcode!(notebook.cells[4], "@asdf 1 + 2") - topo_3 = updated_topology(topo_2, notebook, notebook.cells[4:4]) - @test topo_3 !== topo_2 - @test topo_3 !== topo - - @test topo_3.unresolved_cells |> only === notebook.cells[4] - - @test topo_3 === updated_topology(topo_3, notebook, notebook.cells[1:3]) - @test topo_3 === updated_topology(topo_3, notebook, Cell[]) - # rerunning the cell with the macro does not change the topology because it was already unresolved - @test topo_3 === updated_topology(topo_3, notebook, notebook.cells[1:4]) - - # let's pretend that we resolved the macro in the 4th cell - topo_3_resolved = NotebookTopology(; - nodes=topo_3.nodes, - codes=topo_3.codes, - unresolved_cells=setdiff(topo_3.unresolved_cells, notebook.cells[4:4]), - cell_order=topo_3.cell_order, - disabled_cells=topo_3.disabled_cells, - ) - - @test topo_3_resolved === updated_topology(topo_3_resolved, notebook, notebook.cells[1:3]) - @test topo_3_resolved === updated_topology(topo_3_resolved, notebook, Cell[]) - # rerunning the cell with the macro makes it unresolved again - @test topo_3_resolved !== updated_topology(topo_3_resolved, notebook, notebook.cells[1:4]) - - notebook.cells[4] ∈ updated_topology(topo_3_resolved, notebook, notebook.cells[1:4]).unresolved_cells - - # @test topo_3 === static_resolve_topology(topo_3) - end -end diff --git a/test/React.jl b/test/React.jl index d26b24be54..9d62993977 100644 --- a/test/React.jl +++ b/test/React.jl @@ -2,6 +2,10 @@ using Test import Pluto: Configuration, Notebook, ServerSession, ClientSession, update_run!, Cell, WorkspaceManager import Pluto.Configuration: Options, EvaluationOptions +### MORE TESTS ARE IN PLUTODEPENDENCYEXPLORER.jL +# The tests on the Pluto side are tests that rely more heavily on what Pluto implements on top of PlutoDependencyExplorer. +# The tests in PlutoDependencyExplorer are focus in *reactive ordering*. + @testset "Reactivity" begin 🍭 = ServerSession() 🍭.options.evaluation.workspace_use_distributed = false @@ -93,78 +97,6 @@ import Pluto.Configuration: Options, EvaluationOptions 🍭.options.evaluation.workspace_use_distributed = false - @testset "Mutliple assignments" begin - notebook = Notebook([ - Cell("x = 1"), - Cell("x = 2"), - Cell("f(x) = 3"), - Cell("f(x) = 4"), - Cell("g(x) = 5"), - Cell("g = 6"), - ]) - - - update_run!(🍭, notebook, notebook.cells[1]) - update_run!(🍭, notebook, notebook.cells[2]) - @test occursinerror("Multiple", notebook.cells[1]) - @test occursinerror("Multiple", notebook.cells[2]) - - setcode!(notebook.cells[1], "") - update_run!(🍭, notebook, notebook.cells[1]) - @test notebook.cells[1] |> noerror - @test notebook.cells[2] |> noerror - - # https://github.com/fonsp/Pluto.jl/issues/26 - setcode!(notebook.cells[1], "x = 1") - update_run!(🍭, notebook, notebook.cells[1]) - setcode!(notebook.cells[2], "x") - update_run!(🍭, notebook, notebook.cells[2]) - @test notebook.cells[1] |> noerror - @test notebook.cells[2] |> noerror - - update_run!(🍭, notebook, notebook.cells[3]) - update_run!(🍭, notebook, notebook.cells[4]) - @test occursinerror("Multiple", notebook.cells[3]) - @test occursinerror("Multiple", notebook.cells[4]) - - setcode!(notebook.cells[3], "") - update_run!(🍭, notebook, notebook.cells[3]) - @test notebook.cells[3] |> noerror - @test notebook.cells[4] |> noerror - - update_run!(🍭, notebook, notebook.cells[5]) - update_run!(🍭, notebook, notebook.cells[6]) - @test occursinerror("Multiple", notebook.cells[5]) - @test occursinerror("Multiple", notebook.cells[6]) - - setcode!(notebook.cells[5], "") - update_run!(🍭, notebook, notebook.cells[5]) - @test notebook.cells[5] |> noerror - @test notebook.cells[6] |> noerror - - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - end - - @testset "Mutliple assignments topology" begin - notebook = Notebook([ - Cell("x = 1"), - Cell("z = 4 + y"), - Cell("y = x + 2"), - Cell("y = x + 3"), - ]) - notebook.topology = Pluto.updated_topology(notebook.topology, notebook, notebook.cells) - - let topo_order = Pluto.topological_order(notebook.topology, notebook.cells[[1]]) - @test indexin(topo_order.runnable, notebook.cells) == [1,2] - @test topo_order.errable |> keys == notebook.cells[[3,4]] |> Set - end - let topo_order = Pluto.topological_order(notebook.topology, notebook.cells[[1]], allow_multiple_defs=true) - @test indexin(topo_order.runnable, notebook.cells) == [1,3,4,2] # x first, y second and third, z last - # this also tests whether multiple defs run in page order - @test topo_order.errable == Dict() - end - end - @testset "Simple insert cell" begin notebook = Notebook(Cell[]) update_run!(🍭, notebook, notebook.cells) @@ -217,67 +149,6 @@ import Pluto.Configuration: Options, EvaluationOptions @test notebook.cells[end].output.body == "1" end - @testset "Pkg topology workarounds" begin - notebook = Notebook([ - Cell("1 + 1"), - Cell("json([1,2])"), - Cell("using JSON"), - Cell("""Pkg.add("JSON")"""), - Cell("Pkg.activate(mktempdir())"), - Cell("import Pkg"), - Cell("using Revise"), - Cell("1 + 1"), - ]) - notebook.topology = Pluto.updated_topology(notebook.topology, notebook, notebook.cells) - - topo_order = Pluto.topological_order(notebook.topology, notebook.cells) - @test indexin(topo_order.runnable, notebook.cells) == [6, 5, 4, 7, 3, 1, 2, 8] - # 6, 5, 4, 3 should run first (this is implemented using `cell_precedence_heuristic`), in that order - # 1, 2, 7 remain, and should run in notebook order. - - # if the cells were placed in reverse order... - reverse!(notebook.cell_order) - topo_order = Pluto.topological_order(notebook.topology, notebook.cells) - @test indexin(topo_order.runnable, reverse(notebook.cells)) == [6, 5, 4, 7, 3, 8, 2, 1] - # 6, 5, 4, 3 should run first (this is implemented using `cell_precedence_heuristic`), in that order - # 1, 2, 7 remain, and should run in notebook order, which is 7, 2, 1. - - reverse!(notebook.cell_order) - end - - @testset "Pkg topology workarounds -- hard" begin - notebook = Notebook([ - Cell("json([1,2])"), - Cell("using JSON"), - Cell("Pkg.add(package_name)"), - Cell(""" package_name = "JSON" """), - Cell("Pkg.activate(envdir)"), - Cell("envdir = mktempdir()"), - Cell("import Pkg"), - Cell("using JSON3, Revise"), - ]) - - notebook.topology = Pluto.updated_topology(notebook.topology, notebook, notebook.cells) - - topo_order = Pluto.topological_order(notebook.topology, notebook.cells) - - comesbefore(A, first, second) = findfirst(isequal(first),A) < findfirst(isequal(second), A) - - run_order = indexin(topo_order.runnable, notebook.cells) - - # like in the previous test - @test comesbefore(run_order, 7, 5) - @test_broken comesbefore(run_order, 5, 3) - @test_broken comesbefore(run_order, 3, 2) - @test comesbefore(run_order, 2, 1) - @test comesbefore(run_order, 8, 2) - @test comesbefore(run_order, 8, 1) - - # the variable dependencies - @test comesbefore(run_order, 6, 5) - @test comesbefore(run_order, 4, 3) - end - @testset "Cleanup of workspace variable" begin notebook = Notebook([ Cell("x = 10000"), @@ -334,20 +205,6 @@ import Pluto.Configuration: Options, EvaluationOptions @test !isnothing(Main.var"Pluto#2443".x) end - @testset "Mixed usings and reactivity" begin - notebook = Notebook([ - Cell("a; using Dates"), - Cell("isleapyear(2)"), - Cell("a = 3; using LinearAlgebra"), - ]) - - notebook.topology = Pluto.updated_topology(notebook.topology, notebook, notebook.cells) - topo_order = Pluto.topological_order(notebook.topology, notebook.cells) - run_order = indexin(topo_order.runnable, notebook.cells) - - @test run_order == [3, 1, 2] - end - @testset "Reactive usings" begin notebook = Notebook([ Cell("June"), @@ -475,55 +332,6 @@ import Pluto.Configuration: Options, EvaluationOptions WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) end - @testset "Function dependencies" begin - 🍭.options.evaluation.workspace_use_distributed = true - - notebook = Notebook(Cell.([ - "a'b", - "import LinearAlgebra", - "LinearAlgebra.conj(b::Int) = 2b", - "a = 10", - "b = 10", - ])) - - update_run!(🍭, notebook, notebook.cells) - - @test :conj ∈ notebook.topology.nodes[notebook.cells[3]].soft_definitions - @test :conj ∈ notebook.topology.nodes[notebook.cells[1]].references - @test notebook.cells[1].output.body == "200" - - WorkspaceManager.unmake_workspace((🍭, notebook)) - 🍭.options.evaluation.workspace_use_distributed = false - end - - @testset "Function use inv in its def but also has a method on inv" begin - notebook = Notebook(Cell.([ - """ - struct MyStruct - s - - MyStruct(x) = new(inv(x)) - end - """, - """ - Base.inv(s::MyStruct) = inv(s.s) - """, - "MyStruct(1.) |> inv" - ])) - cell(idx) = notebook.cells[idx] - update_run!(🍭, notebook, notebook.cells) - - @test cell(1) |> noerror - @test cell(2) |> noerror - @test cell(3) |> noerror - - # Empty and run cells to remove the Base overloads that we created, just to be sure - setcode!.(notebook.cells, [""]) - update_run!(🍭, notebook, notebook.cells) - - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - end - @testset "More challenging reactivity of extended function" begin notebook = Notebook(Cell.([ "Base.inv(s::String) = s", @@ -562,39 +370,6 @@ import Pluto.Configuration: Options, EvaluationOptions WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) end - @testset "multiple cells cycle" begin - notebook = Notebook(Cell.([ - "a = inv(1)", - "b = a", - "c = b", - "Base.inv(x::Float64) = a", - "d = Float64(c)", - ])) - update_run!(🍭, notebook, notebook.cells) - - @test all(noerror, notebook.cells) - @test notebook.cells[end].output.body == "1.0" # a - end - - @testset "one cell in two different cycles where one is not a real cycle" begin - notebook = Notebook(Cell.([ - "x = inv(1) + z", - "y = x", - "z = y", - "Base.inv(::Float64) = y", - "inv(1.0)", - ])) - update_run!(🍭, notebook, notebook.cells) - - @test notebook.cells[end].errored == true - @test occursinerror("Cyclic", notebook.cells[1]) - @test expecterror(UndefVarError(:y), notebook.cells[end]) # this is an UndefVarError and not a CyclicError - - setcode!.(notebook.cells, [""]) - update_run!(🍭, notebook, notebook.cells) - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - end - @testset "Reactive methods definitions" begin notebook = Notebook(Cell.([ raw""" @@ -639,391 +414,6 @@ import Pluto.Configuration: Options, EvaluationOptions @test notebook.cells[2] |> noerror end - @testset "Two inter-twined cycles" begin - notebook = Notebook(Cell.([ - """ - begin - struct A - x - A(x) = A(inv(x)) - end - rand() - end - """, - "Base.inv(::A) = A(1)", - """ - struct B - x - B(x) = B(inv(x)) - end - """, - "Base.inv(::B) = B(1)", - ])) - update_run!(🍭, notebook, notebook.cells) - - @test all(noerror, notebook.cells) - output_1 = notebook.cells[begin].output.body - - update_run!(🍭, notebook, notebook.cells[2]) - - @test noerror(notebook.cells[1]) - @test notebook.cells[1].output.body == output_1 - @test noerror(notebook.cells[2]) - - setcode!.(notebook.cells, [""]) - update_run!(🍭, notebook, notebook.cells) - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - end - - @testset "Multiple methods across cells" begin - notebook = Notebook([ - Cell("a(x) = 1"), - Cell("a(x,y) = 2"), - Cell("a(3)"), - Cell("a(4,4)"), - - Cell("b = 5"), - Cell("b(x) = 6"), - Cell("b + 7"), - Cell("b(8)"), - - Cell("Base.tan(x::String) = 9"), - Cell("Base.tan(x::Missing) = 10"), - Cell("Base.tan(\"eleven\")"), - Cell("Base.tan(missing)"), - Cell("tan(missing)"), - - Cell("d(x::Integer) = 14"), - Cell("d(x::String) = 15"), - Cell("d(16)"), - Cell("d(\"seventeen\")"), - Cell("d"), - - Cell("struct asdf; x; y; end"), - Cell(""), - Cell("asdf(21, 21)"), - Cell("asdf(22)"), - - Cell("@enum e1 e2 e3"), - Cell("@enum e4 e5=24"), - Cell("Base.@enum e6 e7=25 e8"), - Cell("Base.@enum e9 e10=26 e11"), - Cell("""@enum e12 begin - e13=27 - e14 - end"""), - ]) - - update_run!(🍭, notebook, notebook.cells[1:4]) - @test notebook.cells[1] |> noerror - @test notebook.cells[2] |> noerror - @test notebook.cells[3].output.body == "1" - @test notebook.cells[4].output.body == "2" - - setcode!(notebook.cells[1], "a(x,x) = 999") - update_run!(🍭, notebook, notebook.cells[1]) - @test notebook.cells[1].errored == true - @test notebook.cells[2].errored == true - @test notebook.cells[3].errored == true - @test notebook.cells[4].errored == true - - setcode!(notebook.cells[1], "a(x) = 1") - update_run!(🍭, notebook, notebook.cells[1]) - @test notebook.cells[1] |> noerror - @test notebook.cells[2] |> noerror - @test notebook.cells[3].output.body == "1" - @test notebook.cells[4].output.body == "2" - - setcode!(notebook.cells[1], "") - update_run!(🍭, notebook, notebook.cells[1]) - @test notebook.cells[1] |> noerror - @test notebook.cells[2] |> noerror - @test notebook.cells[3].errored == true - @test notebook.cells[4].output.body == "2" - - update_run!(🍭, notebook, notebook.cells[5:8]) - @test notebook.cells[5].errored == true - @test notebook.cells[6].errored == true - @test notebook.cells[7].errored == true - @test notebook.cells[8].errored == true - - setcode!(notebook.cells[5], "") - update_run!(🍭, notebook, notebook.cells[5]) - @test notebook.cells[5] |> noerror - @test notebook.cells[6] |> noerror - @test notebook.cells[7].errored == true - @test notebook.cells[8].output.body == "6" - - setcode!(notebook.cells[5], "b = 5") - setcode!(notebook.cells[6], "") - update_run!(🍭, notebook, notebook.cells[5:6]) - @test notebook.cells[5] |> noerror - @test notebook.cells[6] |> noerror - @test notebook.cells[7].output.body == "12" - @test notebook.cells[8].errored == true - - update_run!(🍭, notebook, notebook.cells[11:13]) - @test notebook.cells[12].output.body == "missing" - - update_run!(🍭, notebook, notebook.cells[9:10]) - @test notebook.cells[9] |> noerror - @test notebook.cells[10] |> noerror - @test notebook.cells[11].output.body == "9" - @test notebook.cells[12].output.body == "10" - @test notebook.cells[13].output.body == "10" - update_run!(🍭, notebook, notebook.cells[13]) - @test notebook.cells[13].output.body == "10" - - setcode!(notebook.cells[9], "") - update_run!(🍭, notebook, notebook.cells[9]) - @test notebook.cells[11].errored == true - @test notebook.cells[12].output.body == "10" - - setcode!(notebook.cells[10], "") - update_run!(🍭, notebook, notebook.cells[10]) - @test notebook.cells[11].errored == true - @test notebook.cells[12].output.body == "missing" - - # Cell("d(x::Integer) = 14"), - # Cell("d(x::String) = 15"), - # Cell("d(16)"), - # Cell("d(\"seventeen\")"), - # Cell("d"), - - update_run!(🍭, notebook, notebook.cells[16:18]) - @test notebook.cells[16].errored == true - @test notebook.cells[17].errored == true - @test notebook.cells[18].errored == true - - update_run!(🍭, notebook, notebook.cells[14]) - @test notebook.cells[16] |> noerror - @test notebook.cells[17].errored == true - @test notebook.cells[18] |> noerror - - update_run!(🍭, notebook, notebook.cells[15]) - @test notebook.cells[16] |> noerror - @test notebook.cells[17] |> noerror - @test notebook.cells[18] |> noerror - - setcode!(notebook.cells[14], "") - update_run!(🍭, notebook, notebook.cells[14]) - @test notebook.cells[16].errored == true - @test notebook.cells[17] |> noerror - @test notebook.cells[18] |> noerror - - setcode!(notebook.cells[15], "") - update_run!(🍭, notebook, notebook.cells[15]) - @test notebook.cells[16].errored == true - @test notebook.cells[17].errored == true - @test notebook.cells[18].errored == true - @test occursinerror("UndefVarError", notebook.cells[18]) - - # Cell("struct e; x; y; end"), - # Cell(""), - # Cell("e(21, 21)"), - # Cell("e(22)"), - - update_run!(🍭, notebook, notebook.cells[19:22]) - @test notebook.cells[19] |> noerror - @test notebook.cells[21] |> noerror - @test notebook.cells[22].errored == true - - setcode!(notebook.cells[20], "asdf(x) = asdf(x,x)") - update_run!(🍭, notebook, notebook.cells[20]) - @test occursinerror("Multiple definitions", notebook.cells[19]) - @test occursinerror("Multiple definitions", notebook.cells[20]) - @test occursinerror("asdf", notebook.cells[20]) - @test occursinerror("asdf", notebook.cells[20]) - @test notebook.cells[21].errored == true - @test notebook.cells[22].errored == true - - setcode!(notebook.cells[20], "") - update_run!(🍭, notebook, notebook.cells[20]) - @test notebook.cells[19] |> noerror - @test notebook.cells[20] |> noerror - @test notebook.cells[21] |> noerror - @test notebook.cells[22].errored == true - - setcode!(notebook.cells[19], "begin struct asdf; x; y; end; asdf(x) = asdf(x,x); end") - setcode!(notebook.cells[20], "") - update_run!(🍭, notebook, notebook.cells[19:20]) - @test notebook.cells[19] |> noerror - @test notebook.cells[20] |> noerror - @test notebook.cells[21] |> noerror - @test notebook.cells[22] |> noerror - - update_run!(🍭, notebook, notebook.cells[23:27]) - @test notebook.cells[23] |> noerror - @test notebook.cells[24] |> noerror - @test notebook.cells[25] |> noerror - @test notebook.cells[26] |> noerror - @test notebook.cells[27] |> noerror - update_run!(🍭, notebook, notebook.cells[23:27]) - @test notebook.cells[23] |> noerror - @test notebook.cells[24] |> noerror - @test notebook.cells[25] |> noerror - @test notebook.cells[26] |> noerror - @test notebook.cells[27] |> noerror - - setcode!.(notebook.cells[23:27], [""]) - update_run!(🍭, notebook, notebook.cells[23:27]) - - setcode!(notebook.cells[23], "@assert !any(isdefined.([@__MODULE__], [Symbol(:e,i) for i in 1:14]))") - update_run!(🍭, notebook, notebook.cells[23]) - @test notebook.cells[23] |> noerror - - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - - # for some unsupported edge cases, see: - # https://github.com/fonsp/Pluto.jl/issues/177#issuecomment-645039993 - end - - @testset "Cyclic" begin - notebook = Notebook([ - Cell("xxx = yyy") - Cell("yyy = xxx") - Cell("zzz = yyy") - - Cell("aaa() = bbb") - Cell("bbb = aaa()") - - Cell("w1(x) = w2(x - 1) + 1") - Cell("w2(x) = x > 0 ? w1(x) : x") - Cell("w1(8)") - - Cell("p1(x) = p2(x) + p1(x)") - Cell("p2(x) = p1(x)") - - # 11 - Cell("z(x::String) = z(1)") - Cell("z(x::Integer) = z()") - - # 13 - # some random Base function that we are overloading - Cell("Base.get(x::InterruptException) = Base.get(1)") - Cell("Base.get(x::ArgumentError) = Base.get()") - - Cell("Base.step(x::InterruptException) = step(1)") - Cell("Base.step(x::ArgumentError) = step()") - - Cell("Base.exponent(x::InterruptException) = Base.exponent(1)") - Cell("Base.exponent(x::ArgumentError) = exponent()") - - # 19 - Cell("Base.chomp(x::InterruptException) = split() + chomp()") - Cell("Base.chomp(x::ArgumentError) = chomp()") - Cell("Base.split(x::InterruptException) = split()") - - # 22 - Cell("Base.transpose(x::InterruptException) = Base.trylock() + Base.transpose()") - Cell("Base.transpose(x::ArgumentError) = Base.transpose()") - Cell("Base.trylock(x::InterruptException) = Base.trylock()") - - # 25 - Cell("Base.digits(x::ArgumentError) = Base.digits() + Base.isconst()") - Cell("Base.isconst(x::InterruptException) = digits()") - - # 27 - Cell("f(x) = g(x-1)") - Cell("g(x) = h(x-1)") - Cell("h(x) = i(x-1)") - Cell("i(x) = j(x-1)") - Cell("j(x) = (x > 0) ? f(x-1) : :done") - Cell("f(8)") - ]) - - update_run!(🍭, notebook, notebook.cells[1:3]) - @test occursinerror("Cyclic reference", notebook.cells[1]) - @test occursinerror("xxx", notebook.cells[1]) - @test occursinerror("yyy", notebook.cells[1]) - @test occursinerror("Cyclic reference", notebook.cells[2]) - @test occursinerror("xxx", notebook.cells[2]) - @test occursinerror("yyy", notebook.cells[2]) - @test occursinerror("UndefVarError", notebook.cells[3]) - - setcode!(notebook.cells[1], "xxx = 1") - update_run!(🍭, notebook, notebook.cells[1]) - @test notebook.cells[1].output.body == "1" - @test notebook.cells[2].output.body == "1" - @test notebook.cells[3].output.body == "1" - - setcode!(notebook.cells[1], "xxx = zzz") - update_run!(🍭, notebook, notebook.cells[1]) - @test occursinerror("Cyclic reference", notebook.cells[1]) - @test occursinerror("Cyclic reference", notebook.cells[2]) - @test occursinerror("Cyclic reference", notebook.cells[3]) - @test occursinerror("xxx", notebook.cells[1]) - @test occursinerror("yyy", notebook.cells[1]) - @test occursinerror("zzz", notebook.cells[1]) - @test occursinerror("xxx", notebook.cells[2]) - @test occursinerror("yyy", notebook.cells[2]) - @test occursinerror("zzz", notebook.cells[2]) - @test occursinerror("xxx", notebook.cells[3]) - @test occursinerror("yyy", notebook.cells[3]) - @test occursinerror("zzz", notebook.cells[3]) - - setcode!(notebook.cells[3], "zzz = 3") - update_run!(🍭, notebook, notebook.cells[3]) - @test notebook.cells[1].output.body == "3" - @test notebook.cells[2].output.body == "3" - @test notebook.cells[3].output.body == "3" - - ## - - - update_run!(🍭, notebook, notebook.cells[4:5]) - @test occursinerror("Cyclic reference", notebook.cells[4]) - @test occursinerror("aaa", notebook.cells[4]) - @test occursinerror("bbb", notebook.cells[4]) - @test occursinerror("Cyclic reference", notebook.cells[5]) - @test occursinerror("aaa", notebook.cells[5]) - @test occursinerror("bbb", notebook.cells[5]) - - - - - - update_run!(🍭, notebook, notebook.cells[6:end]) - @test noerror(notebook.cells[6]) - @test noerror(notebook.cells[7]) - @test noerror(notebook.cells[8]) - @test noerror(notebook.cells[9]) - @test noerror(notebook.cells[10]) - @test noerror(notebook.cells[11]) - @test noerror(notebook.cells[12]) - @test noerror(notebook.cells[13]) - @test noerror(notebook.cells[14]) - @test noerror(notebook.cells[15]) - @test noerror(notebook.cells[16]) - @test noerror(notebook.cells[17]) - @test noerror(notebook.cells[18]) - @test noerror(notebook.cells[19]) - @test noerror(notebook.cells[20]) - @test noerror(notebook.cells[21]) - @test noerror(notebook.cells[22]) - @test noerror(notebook.cells[23]) - @test noerror(notebook.cells[24]) - @test noerror(notebook.cells[25]) - @test noerror(notebook.cells[26]) - - ## - @test noerror(notebook.cells[27]) - @test noerror(notebook.cells[28]) - @test noerror(notebook.cells[29]) - @test noerror(notebook.cells[30]) - @test noerror(notebook.cells[31]) - @test noerror(notebook.cells[32]) - @test notebook.cells[32].output.body == ":done" - - @assert length(notebook.cells) == 32 - - # Empty and run cells to remove the Base overloads that we created, just to be sure - setcode!.(notebook.cells, [""]) - update_run!(🍭, notebook, notebook.cells) - - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - end - @testset "Variable deletion" begin notebook = Notebook([ Cell("x = 1"), @@ -1060,39 +450,6 @@ import Pluto.Configuration: Options, EvaluationOptions WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) end - @testset "Recursion" begin - notebook = Notebook([ - Cell("f(n) = n * f(n-1)"), - - Cell("k = 1"), - Cell("""begin - g(n) = h(n-1) + k - h(n) = n > 0 ? g(n-1) : 0 - end"""), - - Cell("h(4)"), - ]) - - update_run!(🍭, notebook, notebook.cells[1]) - @test notebook.cells[1].output.body == "f" || startswith(notebook.cells[1].output.body, "f (generic function with ") - @test notebook.cells[1] |> noerror - - update_run!(🍭, notebook, notebook.cells[2:3]) - @test notebook.cells[2] |> noerror - @test notebook.cells[3] |> noerror - update_run!(🍭, notebook, notebook.cells[3]) - @test notebook.cells[3] |> noerror - - update_run!(🍭, notebook, notebook.cells[4]) - @test notebook.cells[4].output.body == "2" - - setcode!(notebook.cells[2], "k = 2") - update_run!(🍭, notebook, notebook.cells[2]) - @test notebook.cells[4].output.body == "4" - - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - end - @testset "Variable cannot reference its previous value" begin notebook = Notebook([ Cell("x = 3") @@ -1310,47 +667,6 @@ import Pluto.Configuration: Options, EvaluationOptions end WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - @testset "Functional programming" begin - notebook = Notebook([ - Cell("a = 1"), - Cell("map(2:2) do val; (a = val; 2*val) end |> last"), - - Cell("b = 3"), - Cell("g = f"), - Cell("f(x) = x + b"), - Cell("g(6)"), - - Cell("h = [x -> x + b][1]"), - Cell("h(8)"), - ]) - - update_run!(🍭, notebook, notebook.cells[1:2]) - @test notebook.cells[1].output.body == "1" - @test notebook.cells[2].output.body == "4" - - update_run!(🍭, notebook, notebook.cells[3:6]) - @test notebook.cells[3] |> noerror - @test notebook.cells[4] |> noerror - @test notebook.cells[5] |> noerror - @test notebook.cells[6] |> noerror - @test notebook.cells[6].output.body == "9" - - setcode!(notebook.cells[3], "b = -3") - update_run!(🍭, notebook, notebook.cells[3]) - @test notebook.cells[6].output.body == "3" - - update_run!(🍭, notebook, notebook.cells[7:8]) - @test notebook.cells[7] |> noerror - @test notebook.cells[8].output.body == "5" - - setcode!(notebook.cells[3], "b = 3") - update_run!(🍭, notebook, notebook.cells[3]) - @test notebook.cells[8].output.body == "11" - - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - - end - @testset "Global assignments inside functions" begin # We currently have a slightly relaxed version of immutable globals: # globals can only be mutated/assigned _in a single cell_. @@ -1667,91 +983,6 @@ import Pluto.Configuration: Options, EvaluationOptions end end - @testset "Run multiple" begin - notebook = Notebook([ - Cell("x = []"), - Cell("b = a + 2; push!(x,2)"), - Cell("c = b + a; push!(x,3)"), - Cell("a = 1; push!(x,4)"), - Cell("a + b +c; push!(x,5)"), - - Cell("a = 1; push!(x,6)"), - - Cell("n = m; push!(x,7)"), - Cell("m = n; push!(x,8)"), - Cell("n = 1; push!(x,9)"), - - Cell("push!(x,10)"), - Cell("push!(x,11)"), - Cell("push!(x,12)"), - Cell("push!(x,13)"), - Cell("push!(x,14)"), - - Cell("join(x, '-')"), - - Cell("φ(16)"), - Cell("φ(χ) = χ + υ"), - Cell("υ = 18"), - - Cell("f(19)"), - Cell("f(x) = x + g(x)"), - Cell("g(x) = x + y"), - Cell("y = 22"), - ]) - - update_run!(🍭, notebook, notebook.cells[1]) - - @testset "Basic" begin - update_run!(🍭, notebook, notebook.cells[2:5]) - - update_run!(🍭, notebook, notebook.cells[15]) - @test notebook.cells[15].output.body == "\"4-2-3-5\"" - end - - @testset "Errors" begin - update_run!(🍭, notebook, notebook.cells[6:9]) - - # should all err, no change to `x` - update_run!(🍭, notebook, notebook.cells[15]) - @test notebook.cells[15].output.body == "\"4-2-3-5\"" - end - - @testset "Maintain order when possible" begin - update_run!(🍭, notebook, notebook.cells[10:14]) - - update_run!(🍭, notebook, notebook.cells[15]) - @test notebook.cells[15].output.body == "\"4-2-3-5-10-11-12-13-14\"" - - update_run!(🍭, notebook, notebook.cells[1]) # resets `x`, only 10-14 should run, in order - @test notebook.cells[15].output.body == "\"10-11-12-13-14\"" - update_run!(🍭, notebook, notebook.cells[15]) - @test notebook.cells[15].output.body == "\"10-11-12-13-14\"" - end - - - update_run!(🍭, notebook, notebook.cells[16:18]) - @test notebook.cells[16] |> noerror - @test notebook.cells[16].output.body == "34" - @test notebook.cells[17] |> noerror - @test notebook.cells[18] |> noerror - - setcode!(notebook.cells[18], "υ = 8") - update_run!(🍭, notebook, notebook.cells[18]) - @test notebook.cells[16].output.body == "24" - - update_run!(🍭, notebook, notebook.cells[19:22]) - @test notebook.cells[19] |> noerror - @test notebook.cells[19].output.body == "60" - @test notebook.cells[20] |> noerror - @test notebook.cells[21] |> noerror - @test notebook.cells[22] |> noerror - - setcode!(notebook.cells[22], "y = 0") - update_run!(🍭, notebook, notebook.cells[22]) - @test notebook.cells[19].output.body == "38" - - WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) - end @testset "Broadcast bug - Issue #2211" begin notebook = Notebook(Cell.([ diff --git a/test/data structures.jl b/test/data structures.jl deleted file mode 100644 index 86657b145b..0000000000 --- a/test/data structures.jl +++ /dev/null @@ -1,104 +0,0 @@ -import Pluto.PlutoDependencyExplorer: ImmutableVector, ImmutableSet, ImmutableDefaultDict, setdiffkeys - -@testset "ImmutableCollections" begin - - - - - -# ╔═╡ bd27d82e-62d6-422c-8fbe-61993dc4c268 -@test isempty(ImmutableVector{Int}()) - -# ╔═╡ d4f2016a-b093-4619-9ccb-3e99bf6fdc9b -@test ImmutableVector{Int32}([1,2,3]).c |> eltype == Int32 - -# ╔═╡ 055f21c0-3741-4762-ac4e-4c89633afbc4 -let - x = [1,2,3] - y = ImmutableVector(x) - push!(x,4) - @test y == [1,2,3] -end - -# ╔═╡ 52310ade-6e06-4ab8-8589-444c161cd93b -let - x = [1,2,3] - y = ImmutableVector{Int32}(x) - push!(x,4) - @test y == [1,2,3] -end - -# ╔═╡ d61600f0-2202-4228-8d35-380f732214e7 -ImmutableSet() - -# ╔═╡ d3871580-cd22-48c1-a1fd-d13a7f2f2135 -ImmutableSet{Int}() - -# ╔═╡ 2af00467-8bbf-49d8-bfe3-6f8d6307e900 -ImmutableSet{Int64}(Set([1,2]); skip_copy=true) - -# ╔═╡ 46836112-7c5c-4ffd-8d1e-93a2c8990b20 -@test ImmutableSet{Int64}(Set([1,2]); skip_copy=true) == Set([1,2]) - -# ╔═╡ fd687b2e-8bec-48b2-810e-38ef00bf567b -let - x = [1.1,2,3] - y = ImmutableVector(x; skip_copy=true) - push!(x,4) - @test y == [1.1,2,3,4] -end - -# ╔═╡ f4dddf0b-cf0a-41d0-880e-6a8fac7c60cb -let - x = [1.1,2,3] - y = ImmutableVector{Float64}(x; skip_copy=true) - push!(x,4) - @test y == [1.1,2,3,4] -end - -# ╔═╡ 25c78371-f12d-44ae-b180-32b88d3aa4f5 -@test eltype(ImmutableSet([2,3,4])) == Int - -# ╔═╡ 45115ac6-6586-458c-83e6-d661c2ce8db2 -let - x = Set([1,2,3]) - y = ImmutableSet(x) - push!(x,4) - @test y == Set([1,2,3]) -end - -# ╔═╡ 4f26640d-31d2-44c4-bbba-82c18d7497ae -let - x = Set([1.1,2,3]) - y = ImmutableSet(x; skip_copy=true) - push!(x,4) - @test y == Set([1.1,2,3,4]) -end - -# ╔═╡ eac9c95b-a2b6-4f1f-8cce-a2ad4c0972c5 -@test union(ImmutableSet([1,2]),[2,3]) == ImmutableSet([1,2,3]) - -# ╔═╡ bff65a2c-8654-4403-8e34-58aac8616729 -@test filter(x -> true, ImmutableVector([1,2,3])) == [1,2,3] - -# ╔═╡ ce3cdb24-e851-4cc3-9955-b34fe358b41a -@test ImmutableVector([1,2,3])[2:end] isa ImmutableVector - -# ╔═╡ c61196d6-f529-4883-b334-ed1b0f653acf - - -# ╔═╡ 5c2b3440-7231-42df-b4e5-619001d225a8 -ImmutableSet([123,234]) - - - -@test setdiffkeys(Dict(1=>2,3=>4),[3]) == Dict(1=>2) - -let - d = setdiffkeys(ImmutableDefaultDict(() -> 7, Dict(1=>2,3=>4)),[3]) - @test d[1] == 2 && d[3] == 7 -end - -@test setdiff(ImmutableSet([1,2]), [2]) isa ImmutableSet - -end \ No newline at end of file diff --git a/test/is_just_text.jl b/test/is_just_text.jl new file mode 100644 index 0000000000..7c2929d848 --- /dev/null +++ b/test/is_just_text.jl @@ -0,0 +1,76 @@ +using Test +import Pluto: Notebook, Cell, updated_topology, static_resolve_topology, is_just_text, NotebookTopology + +@testset "is_just_text" begin + notebook = Notebook([ + Cell(""), + Cell("md\"a\""), + Cell("html\"a\""), + Cell("md\"a \$b\$\""), + Cell("md\"a ``b``\""), + Cell(""" + let + x = md"a" + md"r \$x" + end + """), + Cell("html\"a 7 \$b\""), + + Cell("md\"a 8 \$b\""), + Cell("@a md\"asdf 9\""), + Cell("x()"), + Cell("x() = y()"), + Cell("12 + 12"), + Cell("import Dates"), + Cell("import Dates"), + Cell("while false end"), + Cell("for i in [16]; end"), + Cell("[i for i in [17]]"), + Cell("module x18 end"), + Cell(""" + module x19 + exit() + end + """), + Cell("""quote end"""), + Cell("""quote x = 21 end"""), + Cell("""quote \$(x = 22) end"""), + Cell("""asdf"23" """), + Cell("""@asdf("24") """), + Cell("""@x"""), + Cell("""@y z 26"""), + Cell("""f(g"27")"""), + ]) + + old = notebook.topology + new = notebook.topology = updated_topology(old, notebook, notebook.cells) + + @test is_just_text(new, notebook.cells[1]) + @test is_just_text(new, notebook.cells[2]) + @test is_just_text(new, notebook.cells[3]) + @test is_just_text(new, notebook.cells[4]) + @test is_just_text(new, notebook.cells[5]) + @test is_just_text(new, notebook.cells[6]) + @test is_just_text(new, notebook.cells[7]) + + @test !is_just_text(new, notebook.cells[8]) + @test !is_just_text(new, notebook.cells[9]) + @test !is_just_text(new, notebook.cells[10]) + @test !is_just_text(new, notebook.cells[11]) + @test !is_just_text(new, notebook.cells[12]) + @test !is_just_text(new, notebook.cells[13]) + @test !is_just_text(new, notebook.cells[14]) + @test !is_just_text(new, notebook.cells[15]) + @test !is_just_text(new, notebook.cells[16]) + @test !is_just_text(new, notebook.cells[17]) + @test !is_just_text(new, notebook.cells[18]) + @test !is_just_text(new, notebook.cells[19]) + @test !is_just_text(new, notebook.cells[20]) + @test !is_just_text(new, notebook.cells[21]) + @test !is_just_text(new, notebook.cells[22]) + @test !is_just_text(new, notebook.cells[23]) + @test !is_just_text(new, notebook.cells[24]) + @test !is_just_text(new, notebook.cells[25]) + @test !is_just_text(new, notebook.cells[26]) + @test !is_just_text(new, notebook.cells[27]) +end diff --git a/test/runtests.jl b/test/runtests.jl index 72bbfedff1..3653efc485 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -40,9 +40,8 @@ verify_no_running_processes() @timeit_include("packages/PkgCompat.jl") @timeit_include("MethodSignatures.jl") @timeit_include("MoreAnalysis.jl") -@timeit_include("Analysis.jl") +@timeit_include("is_just_text.jl") @timeit_include("webserver_utils.jl") -@timeit_include("data structures.jl") @timeit_include("DependencyCache.jl") @timeit_include("Throttled.jl") @timeit_include("cell_disabling.jl")