using Test import UUIDs import Pluto: PlutoRunner, Notebook, WorkspaceManager, Cell, ServerSession, ClientSession, update_run! import Memoize: @memoize @testset "Macro analysis" begin 🍭 = ServerSession() 🍭.options.evaluation.workspace_use_distributed = false @testset "Base macro call" begin notebook = Notebook([ Cell("@enum Fruit 🍎 🍐"), Cell("my_fruit = 🍎"), Cell("jam(fruit::Fruit) = cook(fruit)"), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test [:🍎, :🍐] ⊆ notebook.topology.nodes[cell(1)].definitions @test :Fruit ∈ notebook.topology.nodes[cell(1)].funcdefs_without_signatures @test Symbol("@enum") ∈ notebook.topology.nodes[cell(1)].references @test cell(2) |> noerror @test :🍎 ∈ notebook.topology.nodes[cell(2)].references @test cell(3) |> noerror @test :Fruit ∈ notebook.topology.nodes[cell(3)].references cleanup(🍭, notebook) end @testset "User defined macro 1" begin notebook = Notebook([ Cell("""macro my_assign(sym, val) :(\$(esc(sym)) = \$(val)) end"""), Cell("@my_assign x 1+1"), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test :x ∈ notebook.topology.nodes[cell(2)].definitions @test Symbol("@my_assign") ∈ notebook.topology.nodes[cell(2)].references update_run!(🍭, notebook, notebook.cells) # Works on second time because of old workspace @test :x ∈ notebook.topology.nodes[cell(2)].definitions @test Symbol("@my_assign") ∈ notebook.topology.nodes[cell(2)].references cleanup(🍭, notebook) end @testset "User defined macro 2" begin notebook = Notebook([ Cell("@my_identity(f(123))"), Cell(""), Cell(""), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) setcode!(cell(2), """macro my_identity(expr) esc(expr) end""") update_run!(🍭, notebook, cell(2)) setcode!(cell(3), "f(x) = x") update_run!(🍭, notebook, cell(3)) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(1).output.body == "123" update_run!(🍭, notebook, cell(1)) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror cleanup(🍭, notebook) end @testset "User defined macro 3" begin notebook = Notebook([ Cell(""" macro mymap() quote [1, 2, 3] .|> sqrt end end """), Cell("@mymap") ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror update_run!(🍭, notebook, cell(1)) @test cell(2) |> noerror cleanup(🍭, notebook) end @testset "User defined macro 4" begin notebook = Notebook([ Cell("""macro my_assign(ex) esc(ex) end"""), Cell("@macroexpand @my_assign 1+1"), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test Symbol("@my_assign") ∈ notebook.topology.nodes[cell(2)].references cleanup(🍭, notebook) end @testset "User defined macro 5" begin notebook = Notebook([ Cell("""macro dynamic_values(ex) [:a, :b, :c] end"""), Cell("myarray = @dynamic_values()"), ]) references(idx) = notebook.topology.nodes[notebook.cells[idx]].references update_run!(🍭, notebook, notebook.cells) @test :a ∉ references(2) @test :b ∉ references(2) @test :c ∉ references(2) cleanup(🍭, notebook) end @testset "User defined macro 6" begin notebook = Notebook([ Cell("""macro my_macro() esc(:(y + x)) end"""), Cell("""function my_function() @my_macro() end"""), Cell("my_function()"), Cell("x = 1"), Cell("y = 2"), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test [Symbol("@my_macro"), :x, :y] ⊆ notebook.topology.nodes[cell(2)].references @test cell(3).output.body == "3" cleanup(🍭, notebook) end @testset "Function docs" begin notebook = Notebook([ Cell(""" "my function doc" f(x) = 2x """), Cell("f"), ]) cell(idx) = notebook.cells[idx] temp_topology = Pluto.updated_topology(notebook.topology, notebook, notebook.cells) |> Pluto.static_resolve_topology # @test :f ∈ temp_topology.nodes[cell(1)].funcdefs_without_signatures update_run!(🍭, notebook, notebook.cells) @test :f ∈ notebook.topology.nodes[cell(1)].funcdefs_without_signatures @test :f ∈ notebook.topology.nodes[cell(2)].references cleanup(🍭, notebook) end @testset "Expr sanitization" begin struct A; end f(x) = x unserializable_expr = :($(f)(A(), A[A(), A(), A()], PlutoRunner, PlutoRunner.sanitize_expr)) get_expr_types(other) = typeof(other) get_expr_types(ex::Expr) = get_expr_types.(ex.args) flatten(x, acc=[]) = push!(acc, x) function flatten(arr::AbstractVector, acc=[]) foreach(x -> flatten(x, acc), arr); acc end sanitized_expr = PlutoRunner.sanitize_expr(unserializable_expr) types = sanitized_expr |> get_expr_types |> flatten |> Set # Checks that no fancy type is part of the serialized expression @test Set([Nothing, Symbol, QuoteNode]) == types end @testset "Macrodef cells not root of run" begin notebook = Notebook([ Cell(""), Cell(""), Cell(""), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test all(noerror, notebook.cells) setcode!(cell(1), raw""" macro test(sym) esc(:($sym = true)) end """) update_run!(🍭, notebook, cell(1)) setcode!(cell(2), "x") setcode!(cell(3), "@test x") update_run!(🍭, notebook, notebook.cells[2:3]) @test cell(2).output.body == "true" @test all(noerror, notebook.cells) cleanup(🍭, notebook) end @testset "Reverse order" begin notebook = Notebook([Cell() for _ in 1:3]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) setcode!(cell(1), "x") update_run!(🍭, notebook, cell(1)) @test cell(1).errored == true setcode!(cell(2), "@bar x") update_run!(🍭, notebook, cell(2)) @test cell(1).errored == true @test cell(2).errored == true setcode!(cell(3), raw"""macro bar(sym) esc(:($sym = "yay")) end""") update_run!(🍭, notebook, cell(3)) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(1).output.body == "\"yay\"" cleanup(🍭, notebook) end @testset "@a defines @b" begin notebook = Notebook([Cell() for _ in 1:4]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) setcode!(cell(1), "x") update_run!(🍭, notebook, cell(1)) @test cell(1).errored == true setcode!(cell(3), "@a()") setcode!(cell(2), raw"""macro a() quote macro b(sym) esc(:($sym = 42)) end end |> esc end""") update_run!(🍭, notebook, notebook.cells[2:3]) @test cell(1).errored == true @test cell(2) |> noerror @test cell(3) |> noerror setcode!(cell(4), "@b x") update_run!(🍭, notebook, cell(4)) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(4) |> noerror @test cell(1).output.body == "42" cleanup(🍭, notebook) end @testset "Removing macros undefvar errors dependent cells" begin notebook = Notebook(Cell.([ """macro m() :(1 + 1) end""", "@m()", ])) update_run!(🍭, notebook, notebook.cells) @test all(noerror, notebook.cells) setcode!(notebook.cells[begin], "") # remove definition of m update_run!(🍭, notebook, notebook.cells[begin]) @test notebook.cells[begin] |> noerror @test notebook.cells[end].errored @test expecterror(UndefVarError(Symbol("@m")), notebook.cells[end]; strict=VERSION >= v"1.7") cleanup(🍭, notebook) end @testset "Redefines macro with new SymbolsState" begin notebook = Notebook(Cell.([ "@b x", raw"""macro b(sym) esc(:($sym = 42)) end""", "x", "y", ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test cell(3).output.body == "42" @test cell(4).errored == true setcode!(cell(2), """macro b(_) esc(:(y = 42)) end""") update_run!(🍭, notebook, cell(2)) @test cell(4).output.body == "42" @test cell(3).errored == true notebook = Notebook(Cell.([ "@b x", raw"""macro b(sym) esc(:($sym = 42)) end""", "x", "y", ])) update_run!(🍭, notebook, notebook.cells) @test cell(3).output.body == "42" @test cell(4).errored == true setcode!(cell(2), """macro b(_) esc(:(y = 42)) end""") update_run!(🍭, notebook, [cell(1), cell(2)]) # Cell 4 is executed even because cell(1) is in the root # of the reactive run because the expansion is done with the new version # of the macro in the new workspace because of the current_roots parameter of `resolve_topology`. # See Run.jl#resolve_topology. @test cell(4).output.body == "42" @test cell(3).errored == true cleanup(🍭, notebook) end @testset "Reactive macro update does not invalidate the macro calls" begin notebook = Notebook(Cell.([ raw"""macro b(sym) if z > 40 esc(:($sym = $z)) else esc(:(y = $z)) end end""", "z = 42", "@b(x)", "x", "y", ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(4) |> noerror @test cell(5).errored == true setcode!(cell(2), "z = 39") # running only 2, running all cells here works however update_run!(🍭, notebook, cell(2)) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(4).output.body != "42" @test cell(4).errored == true @test cell(5) |> noerror cleanup(🍭, notebook) end @testset "Explicitely running macrocalls updates the reactive node" begin notebook = Notebook(Cell.([ "@b()", "ref = Ref{Int}(0)", raw"""macro b() ex = if iseven(ref[]) :(x = 10) else :(y = 10) end |> esc ref[] += 1 ex end""", "x", "y", ])) cell(i) = notebook.cells[i] update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(4) |> noerror @test cell(5).errored == true update_run!(🍭, notebook, cell(1)) @test cell(4).errored == true @test cell(5) |> noerror cleanup(🍭, notebook) end @testset "Implicitely running macrocalls updates the reactive node" begin notebook = Notebook(Cell.([ "updater; @b()", "ref = Ref{Int}(0)", raw"""macro b() ex = if iseven(ref[]) :(x = 10) else :(y = 10) end |> esc ref[] += 1 ex end""", "x", "y", "updater = 1", ])) cell(i) = notebook.cells[i] update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(4) |> noerror output_1 = cell(4).output.body @test cell(5).errored == true @test cell(6) |> noerror setcode!(cell(6), "updater = 2") update_run!(🍭, notebook, cell(6)) # the output of cell 4 has not changed since the underlying computer # has not been regenerated. To update the reactive node and macrocall # an explicit run of @b() must be done. @test cell(4).output.body == output_1 @test cell(5).errored == true cleanup(🍭, notebook) end @testset "Weird behavior" begin # https://github.com/fonsp/Pluto.jl/issues/1591 notebook = Notebook(Cell.([ "macro huh(_) throw(\"Fail!\") end", "huh(e) = e", "@huh(z)", "z = 101010", ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test cell(3).errored == true setcode!(cell(3), "huh(z)") update_run!(🍭, notebook, cell(3)) @test cell(3) |> noerror @test cell(3).output.body == "101010" setcode!(cell(4), "z = 1234") update_run!(🍭, notebook, cell(4)) @test cell(3) |> noerror @test cell(3).output.body == "1234" cleanup(🍭, notebook) end @testset "Cell failing first not re-run?" begin notebook = Notebook(Cell.([ "x", "@b x", raw"macro b(sym) esc(:($sym = 42)) end", ])) update_run!(🍭, notebook, notebook.cells) # CELL 1 "x" was run first and failed because the definition # of x was not yet found. However, it was not run re-run when the definition of # x ("@b(x)") was run. Should it? Maybe set a higher precedence to cells that define # macros inside the notebook. @test_broken noerror(notebook.cells[1]; verbose=false) cleanup(🍭, notebook) end @testset "@a defines @b initial loading" begin notebook = Notebook(Cell.([ "x", "@b x", "@a", raw"""macro a() quote macro b(sym) esc(:($sym = 42)) end end |> esc end""" ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror @test cell(4) |> noerror @test cell(1).output.body == "42" cleanup(🍭, notebook) end @testset "Macro with long compile time gets function wrapped" begin ms = 1e-3 ns = 1e-9 sleep_time = 40ms notebook = Notebook(Cell.([ "updater; @b()", """macro b() x = rand() sleep($sleep_time) :(1+\$x) end""", "updater = :slow", ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test noerror(cell(1)) runtime = cell(1).runtime*ns output_1 = cell(1).output.body @test sleep_time <= runtime setcode!(cell(3), "updater = :fast") update_run!(🍭, notebook, cell(3)) @test noerror(cell(1)) runtime = cell(1).runtime*ns @test runtime < sleep_time # no recompilation! # output is the same because no new compilation happens @test output_1 == cell(1).output.body # force recompilation by explicitely running the cell update_run!(🍭, notebook, cell(1)) @test cell(1) |> noerror @test output_1 != cell(1).output.body output_3 = cell(1).output.body setcode!(cell(1), "@b()") # changing code generates a new 💻 update_run!(🍭, notebook, cell(1)) @test cell(1) |> noerror @test output_3 != cell(1).output.body cleanup(🍭, notebook) end @testset "Macro Prefix" begin 🍭.options.evaluation.workspace_use_distributed = true notebook = Notebook(Cell.([ "@sprintf \"answer = %d\" x", "x = y+1", raw""" macro def(sym) esc(:($sym=41)) end """, "@def y", "import Printf: @sprintf", ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, cell(1)) @test cell(1).errored == true update_run!(🍭, notebook, cell(5)) @test expecterror(UndefVarError(:x), cell(1)) update_run!(🍭, notebook, cell(3)) update_run!(🍭, notebook, cell(2)) update_run!(🍭, notebook, cell(4)) @test cell(1) |> noerror cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false end @testset "Package macro 1" begin notebook = Notebook([ Cell("using Dates"), Cell("df = dateformat\"Y-m-d\""), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, cell(2)) @test cell(2).errored == true @test expecterror(UndefVarError(Symbol("@dateformat_str")), cell(2); strict=VERSION >= v"1.7") update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror notebook = Notebook([ Cell("using Dates"), Cell("df = dateformat\"Y-m-d\""), ]) update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror cleanup(🍭, notebook) end @testset "Package macro 2" begin 🍭.options.evaluation.workspace_use_distributed = true notebook = Notebook([ Cell("z = x^2 + y"), Cell("@variables x y"), Cell(""" begin import Pkg Pkg.activate(mktempdir()) Pkg.add(Pkg.PackageSpec(name="Symbolics", version="5.5.1")) import Symbolics: @variables end """), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells[1:2]) @test cell(1).errored == true @test cell(2).errored == true update_run!(🍭, notebook, cell(3)) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror update_run!(🍭, notebook, notebook.cells) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror update_run!(🍭, notebook, cell(2)) @test cell(1) |> noerror @test cell(2) |> noerror setcode!(cell(2), "@variables 🐰 y") update_run!(🍭, notebook, cell(2)) @test cell(1).errored @test cell(2) |> noerror setcode!(cell(1), "z = 🐰^2 + y") update_run!(🍭, notebook, cell(1)) @test cell(1) |> noerror @test cell(2) |> noerror cleanup(🍭, notebook) 🍭.options.evaluation.workspace_use_distributed = false end @testset "Previous workspace for unknowns" begin notebook = Notebook([ Cell("""macro my_identity(expr) expr end"""), Cell("(@__MODULE__, (@my_identity 1 + 1))"), Cell("@__MODULE__"), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, cell(1)) update_run!(🍭, notebook, notebook.cells[2:end]) @test cell(1) |> noerror @test cell(2) |> noerror @test cell(3) |> noerror module_from_cell2 = cell(2).output.body[:elements][1][2][1] module_from_cell3 = cell(3).output.body @test module_from_cell2 == module_from_cell3 cleanup(🍭, notebook) end @testset "Definitions" begin notebook = Notebook([ Cell("""macro my_assign(sym, val) :(\$(esc(sym)) = \$(val)) end"""), Cell("c = :hello"), Cell("@my_assign b c"), Cell("b"), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) update_run!(🍭, notebook, notebook.cells) @test ":hello" == cell(3).output.body @test ":hello" == cell(4).output.body @test :b ∈ notebook.topology.nodes[cell(3)].definitions @test [:c, Symbol("@my_assign")] ⊆ notebook.topology.nodes[cell(3)].references setcode!(notebook.cells[2], "c = :world") update_run!(🍭, notebook, cell(2)) @test ":world" == cell(3).output.body @test ":world" == cell(4).output.body cleanup(🍭, notebook) end @testset "Is just text macros" begin notebook = Notebook(Cell.([ """ md"# Hello world!" """, """ "no julia value here" """, ])) update_run!(🍭, notebook, notebook.cells) @test isempty(notebook.topology.unresolved_cells) cleanup(🍭, notebook) end @testset "Macros using import" begin notebook = Notebook(Cell.([ """ @option "option_a" struct OptionA option_value::option_type end """, "option_type = String", "import Configurations: @option", ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test :option_type ∈ notebook.topology.nodes[cell(1)].references @test cell(1) |> noerror cleanup(🍭, notebook) end @testset "GlobalRefs in macros should be respected" begin notebook = Notebook(Cell.([ """ macro identity(expr) expr end """, """ x = 20 """, """ let x = 10 @identity(x) end """, ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test all(cell.([1,2,3]) .|> noerror) @test cell(3).output.body == "20" cleanup(🍭, notebook) end @testset "GlobalRefs shouldn't break unreached undefined references" begin notebook = Notebook(Cell.([ """ macro get_x_but_actually_not() quote if false x else :this_should_be_returned end end end """, """ @get_x_but_actually_not() """, ])) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test all(cell.([1,2]) .|> noerror) @test cell(2).output.body == ":this_should_be_returned" cleanup(🍭, notebook) end @testset "Doc strings" begin notebook = Notebook(Cell.([ "x = 1", raw""" "::Bool" f(::Bool) = x """, raw""" "::Int" f(::Int) = 1 """, ])) trigger, bool, int = notebook.cells workspace = WorkspaceManager.get_workspace((🍭, notebook)) workspace_module = getproperty(Main, workspace.module_name) # Propose suggestions when no binding is found doc_content, status = PlutoRunner.doc_fetcher("filer", workspace_module) @test status == :👍 @test occursin("Similar results:", doc_content) @test occursin("filter", doc_content) update_run!(🍭, notebook, notebook.cells) @test all(noerror, notebook.cells) @test occursin("::Bool", bool.output.body) @test !occursin("::Int", bool.output.body) @test occursin("::Bool", int.output.body) @test occursin("::Int", int.output.body) setcode!(int, raw""" "::Int new docstring" f(::Int) = 1 """) update_run!(🍭, notebook, int) @test occursin("::Bool", int.output.body) @test occursin("::Int new docstring", int.output.body) update_run!(🍭, notebook, trigger) @test occursin("::Bool", bool.output.body) @test occursin("::Int new docstring", bool.output.body) @test length(eachmatch(r"Bool", bool.output.body) |> collect) == 1 @test length(eachmatch(r"Int", bool.output.body) |> collect) == 1 update_run!(🍭, notebook, trigger) @test length(eachmatch(r"Bool", bool.output.body) |> collect) == 1 setcode!(int, "") update_run!(🍭, notebook, [bool, int]) @test !occursin("::Int", bool.output.body) setcode!(bool, """ "An empty conjugate" Base.conj() = x """) update_run!(🍭, notebook, bool) @test noerror(bool) @test noerror(trigger) @test occursin("An empty conjugate", bool.output.body) @test occursin("complex conjugate", bool.output.body) setcode!(bool, "Docs.doc(conj)") update_run!(🍭, notebook, bool) @test !occursin("An empty conjugate", bool.output.body) @test occursin("complex conjugate", bool.output.body) cleanup(🍭, notebook) end @testset "Delete methods from macros" begin 🍭 = ServerSession() 🍭.options.evaluation.workspace_use_distributed = false notebook = Notebook([ Cell("using Memoize"), Cell(""" macro user_defined() quote struct ASD end custom_func(::ASD) = "ASD" end |> esc end """), Cell("@user_defined"), Cell("methods(custom_func)"), Cell(""" @memoize function memoized_func(a) println("Running") 2a end """), Cell("methods(memoized_func)"), ]) cell(idx) = notebook.cells[idx] update_run!(🍭, notebook, notebook.cells) @test :custom_func ∈ notebook.topology.nodes[cell(3)].funcdefs_without_signatures @test cell(4) |> noerror @test :memoized_func ∈ notebook.topology.nodes[cell(5)].funcdefs_without_signatures @test cell(6) |> noerror cell(3).code = "#=$(cell(3).code)=#" cell(5).code = "#=$(cell(5).code)=#" update_run!(🍭, notebook, notebook.cells) @test :custom_func ∉ notebook.topology.nodes[cell(3)].funcdefs_without_signatures @test expecterror(UndefVarError(:custom_func), cell(4)) @test :memoized_func ∉ notebook.topology.nodes[cell(5)].funcdefs_without_signatures @test expecterror(UndefVarError(:memoized_func), cell(6)) cleanup(🍭, notebook) end end