mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-04 12:04:20 +08:00
* Implement moving cells with keyboard shortcuts * Add tests for cell movement operation * Refactor * Does not mark cells as stale if Elixir cells did not change order
1137 lines
36 KiB
Elixir
1137 lines
36 KiB
Elixir
defmodule LiveBook.Session.DataTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias LiveBook.Session.Data
|
|
alias LiveBook.{Delta, Notebook}
|
|
|
|
describe "new/1" do
|
|
test "called with no arguments defaults to a blank notebook" do
|
|
empty_map = %{}
|
|
|
|
assert %{notebook: %{sections: []}, cell_infos: ^empty_map, section_infos: ^empty_map} =
|
|
Data.new()
|
|
end
|
|
|
|
test "called with a notebook, sets default cell and section infos" do
|
|
cell = Notebook.Cell.new(:elixir)
|
|
section = %{Notebook.Section.new() | cells: [cell]}
|
|
notebook = %{Notebook.new() | sections: [section]}
|
|
|
|
cell_id = cell.id
|
|
section_id = section.id
|
|
|
|
assert %{cell_infos: %{^cell_id => %{}}, section_infos: %{^section_id => %{}}} =
|
|
Data.new(notebook)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :insert_section" do
|
|
test "adds new section to notebook and session info" do
|
|
data = Data.new()
|
|
|
|
operation = {:insert_section, 0, "s1"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [%{id: "s1"}]
|
|
},
|
|
section_infos: %{"s1" => _}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :insert_cell" do
|
|
test "returns an error given invalid section id" do
|
|
data = Data.new()
|
|
operation = {:insert_cell, "nonexistent", 0, :elixir, "c1"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "insert_cell adds new cell to notebook and cell info" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"}
|
|
])
|
|
|
|
operation = {:insert_cell, "s1", 0, :elixir, "c1"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{cells: [%{id: "c1"}]}
|
|
]
|
|
},
|
|
cell_infos: %{"c1" => _}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "initializes client-revision map" do
|
|
client_pid = self()
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client_pid},
|
|
{:insert_section, 0, "s1"}
|
|
])
|
|
|
|
operation = {:insert_cell, "s1", 0, :elixir, "c1"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c1" => %{revision_by_client_pid: %{^client_pid => 0}}}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :delete_section" do
|
|
test "returns an error given invalid section id" do
|
|
data = Data.new()
|
|
operation = {:delete_section, "nonexistent"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "removes the section from notebook and session info, adds to deleted sections" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"}
|
|
])
|
|
|
|
operation = {:delete_section, "s1"}
|
|
empty_map = %{}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: []
|
|
},
|
|
section_infos: ^empty_map,
|
|
deleted_sections: [%{id: "s1"}]
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :delete_cell" do
|
|
test "returns an error given invalid cell id" do
|
|
data = Data.new()
|
|
operation = {:delete_cell, "nonexistent"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "if the cell is evaluating, cencels section evaluation" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"}
|
|
])
|
|
|
|
operation = {:delete_cell, "c1"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c2" => %{evaluation_status: :ready}},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: nil, evaluation_queue: []}}
|
|
}, _actions} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "removes the cell from notebook and session info, adds to deleted cells" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
operation = {:delete_cell, "c1"}
|
|
empty_map = %{}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [%{cells: []}]
|
|
},
|
|
cell_infos: ^empty_map,
|
|
deleted_cells: [%{id: "c1"}]
|
|
}, _actions} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "unqueues the cell if it's queued for evaluation" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"}
|
|
])
|
|
|
|
operation = {:delete_cell, "c2"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
section_infos: %{"s1" => %{evaluation_queue: []}}
|
|
}, _actions} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "marks evaluated child cells as stale" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
# Evaluate both cells
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}},
|
|
{:queue_cell_evaluation, "c2"},
|
|
{:add_cell_evaluation_response, "c2", {:ok, [1, 2, 3]}}
|
|
])
|
|
|
|
operation = {:delete_cell, "c1"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c2" => %{validity_status: :stale}}
|
|
}, _actions} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns forget evaluation action" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
operation = {:delete_cell, "c1"}
|
|
|
|
assert {:ok, _data, [{:forget_evaluation, %{id: "c1"}, %{id: "s1"}}]} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :move_cell" do
|
|
test "returns an error given invalid cell id" do
|
|
data = Data.new()
|
|
operation = {:move_cell, "nonexistent", 1}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error given no offset" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
operation = {:move_cell, "c1", 0}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "given negative offset moves the cell and marks relevant cells as stale" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
# Add cells
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:insert_cell, "s1", 2, :elixir, "c3"},
|
|
{:insert_cell, "s1", 3, :elixir, "c4"},
|
|
# Evaluate cells
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_response, "c1", {:ok, nil}},
|
|
{:queue_cell_evaluation, "c2"},
|
|
{:add_cell_evaluation_response, "c2", {:ok, nil}},
|
|
{:queue_cell_evaluation, "c3"},
|
|
{:add_cell_evaluation_response, "c3", {:ok, nil}},
|
|
{:queue_cell_evaluation, "c4"},
|
|
{:add_cell_evaluation_response, "c4", {:ok, nil}}
|
|
])
|
|
|
|
operation = {:move_cell, "c3", -1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{
|
|
cells: [%{id: "c1"}, %{id: "c3"}, %{id: "c2"}, %{id: "c4"}]
|
|
}
|
|
]
|
|
},
|
|
cell_infos: %{
|
|
"c1" => %{validity_status: :evaluated},
|
|
"c2" => %{validity_status: :stale},
|
|
"c3" => %{validity_status: :stale},
|
|
"c4" => %{validity_status: :stale}
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "given positive offset moves the cell and marks relevant cells as stale" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
# Add cells
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:insert_cell, "s1", 2, :elixir, "c3"},
|
|
{:insert_cell, "s1", 3, :elixir, "c4"},
|
|
# Evaluate cells
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_response, "c1", {:ok, nil}},
|
|
{:queue_cell_evaluation, "c2"},
|
|
{:add_cell_evaluation_response, "c2", {:ok, nil}},
|
|
{:queue_cell_evaluation, "c3"},
|
|
{:add_cell_evaluation_response, "c3", {:ok, nil}},
|
|
{:queue_cell_evaluation, "c4"},
|
|
{:add_cell_evaluation_response, "c4", {:ok, nil}}
|
|
])
|
|
|
|
operation = {:move_cell, "c2", 1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{
|
|
cells: [%{id: "c1"}, %{id: "c3"}, %{id: "c2"}, %{id: "c4"}]
|
|
}
|
|
]
|
|
},
|
|
cell_infos: %{
|
|
"c1" => %{validity_status: :evaluated},
|
|
"c2" => %{validity_status: :stale},
|
|
"c3" => %{validity_status: :stale},
|
|
"c4" => %{validity_status: :stale}
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "moving a markdown cell does not change validity" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
# Add cells
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :markdown, "c2"},
|
|
# Evaluate the Elixir cell
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_response, "c1", {:ok, nil}}
|
|
])
|
|
|
|
operation = {:move_cell, "c2", -1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c1" => %{validity_status: :evaluated}
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "affected queued cell is unqueued" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
# Add cells
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
# Evaluate the Elixir cell
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"}
|
|
])
|
|
|
|
operation = {:move_cell, "c2", -1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c2" => %{evaluation_status: :ready}
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "does not invalidate the moved cell if the order of Elixir cells stays the same" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
# Add cells
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :markdown, "c2"},
|
|
{:insert_cell, "s1", 2, :elixir, "c3"},
|
|
# Evaluate cells
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_response, "c1", {:ok, nil}},
|
|
{:queue_cell_evaluation, "c3"},
|
|
{:add_cell_evaluation_response, "c3", {:ok, nil}}
|
|
])
|
|
|
|
operation = {:move_cell, "c1", 1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c1" => %{validity_status: :evaluated},
|
|
"c3" => %{validity_status: :evaluated}
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :queue_cell_evaluation" do
|
|
test "returns an error given invalid cell id" do
|
|
data = Data.new()
|
|
operation = {:queue_cell_evaluation, "nonexistent"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error given non-elixir cell" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :markdown, "c1"}
|
|
])
|
|
|
|
operation = {:queue_cell_evaluation, "c1"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error for an evaluating cell" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:queue_cell_evaluation, "c1"}
|
|
])
|
|
|
|
operation = {:queue_cell_evaluation, "c1"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "marks the cell as evaluating if the corresponding section is idle" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
operation = {:queue_cell_evaluation, "c1"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c1" => %{evaluation_status: :evaluating}},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: "c1", evaluation_queue: []}}
|
|
}, _actions} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns start evaluation action if the corresponding section is idle" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
operation = {:queue_cell_evaluation, "c1"}
|
|
|
|
assert {:ok, _data, [{:start_evaluation, %{id: "c1"}, %{id: "s1"}}]} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "marks the cell as queued if the corresponding section is already evaluating" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"}
|
|
])
|
|
|
|
operation = {:queue_cell_evaluation, "c2"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c2" => %{evaluation_status: :queued}},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: "c1", evaluation_queue: ["c2"]}}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :add_cell_evaluation_stdout" do
|
|
test "updates the cell outputs" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:queue_cell_evaluation, "c1"}
|
|
])
|
|
|
|
operation = {:add_cell_evaluation_stdout, "c1", "Hello!"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{
|
|
cells: [%{outputs: ["Hello!"]}]
|
|
}
|
|
]
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "merges consecutive stdout results" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_stdout, "c1", "Hello"}
|
|
])
|
|
|
|
operation = {:add_cell_evaluation_stdout, "c1", " amigo!"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{
|
|
cells: [%{outputs: ["Hello amigo!"]}]
|
|
}
|
|
]
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :add_cell_evaluation_response" do
|
|
test "updates the cell outputs" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:queue_cell_evaluation, "c1"}
|
|
])
|
|
|
|
operation = {:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{
|
|
cells: [%{outputs: [{:ok, [1, 2, 3]}]}]
|
|
}
|
|
]
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "marks the cell as evaluated" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"}
|
|
])
|
|
|
|
operation = {:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c1" => %{validity_status: :evaluated, evaluation_status: :ready}},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: nil, evaluation_queue: []}}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "marks next queued cell in this section as evaluating if there is one" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"}
|
|
])
|
|
|
|
operation = {:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c2" => %{evaluation_status: :evaluating}},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: "c2", evaluation_queue: []}}
|
|
},
|
|
[{:start_evaluation, %{id: "c2"}, %{id: "s1"}}]} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "if parent cells are not executed, marks them for evaluation first" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"}
|
|
])
|
|
|
|
operation = {:queue_cell_evaluation, "c2"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c1" => %{evaluation_status: :evaluating},
|
|
"c2" => %{evaluation_status: :queued}
|
|
},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: "c1", evaluation_queue: ["c2"]}}
|
|
},
|
|
[{:start_evaluation, %{id: "c1"}, %{id: "s1"}}]} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "marks evaluated child cells as stale" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
# Evaluate both cells
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}},
|
|
{:queue_cell_evaluation, "c2"},
|
|
{:add_cell_evaluation_response, "c2", {:ok, [1, 2, 3]}},
|
|
# Queue the first cell again
|
|
{:queue_cell_evaluation, "c1"}
|
|
])
|
|
|
|
operation = {:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c2" => %{validity_status: :stale}}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :cancel_cell_evaluation" do
|
|
test "returns an error given invalid cell id" do
|
|
data = Data.new()
|
|
operation = {:cancel_cell_evaluation, "nonexistent"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error for an evaluated cell" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:add_cell_evaluation_response, "c1", {:ok, [1, 2, 3]}}
|
|
])
|
|
|
|
operation = {:cancel_cell_evaluation, "c1"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "if the cell is evaluating, clears the corresponding section evaluation and the queue" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"}
|
|
])
|
|
|
|
operation = {:cancel_cell_evaluation, "c1"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c1" => %{validity_status: :fresh},
|
|
"c2" => %{validity_status: :fresh}
|
|
},
|
|
section_infos: %{
|
|
"s1" => %{evaluating_cell_id: nil, evaluation_queue: []}
|
|
}
|
|
}, _actions} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "if the cell is evaluating, returns stop evaluation action" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"}
|
|
])
|
|
|
|
operation = {:cancel_cell_evaluation, "c1"}
|
|
|
|
assert {:ok, _data, [{:stop_evaluation, %{id: "s1"}}]} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "if the cell is queued, unqueues it" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"}
|
|
])
|
|
|
|
operation = {:cancel_cell_evaluation, "c2"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c2" => %{validity_status: :fresh, evaluation_status: :ready}},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: "c1", evaluation_queue: []}}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "if the cell is queued, unqueues dependent cells that are also queued" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:insert_cell, "s1", 2, :elixir, "c3"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"},
|
|
{:queue_cell_evaluation, "c3"}
|
|
])
|
|
|
|
operation = {:cancel_cell_evaluation, "c2"}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c3" => %{evaluation_status: :ready}},
|
|
section_infos: %{"s1" => %{evaluating_cell_id: "c1", evaluation_queue: []}}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :set_notebook_name" do
|
|
test "updates notebook name with the given string" do
|
|
data = Data.new()
|
|
|
|
operation = {:set_notebook_name, "Cat's guide to life"}
|
|
|
|
assert {:ok, %{notebook: %{name: "Cat's guide to life"}}, []} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :set_section_name" do
|
|
test "returns an error given invalid cell id" do
|
|
data = Data.new()
|
|
operation = {:set_section_name, "nonexistent", "Chapter 1"}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "updates section name with the given string" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"}
|
|
])
|
|
|
|
operation = {:set_section_name, "s1", "Cat's guide to life"}
|
|
|
|
assert {:ok, %{notebook: %{sections: [%{name: "Cat's guide to life"}]}}, []} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :client_join" do
|
|
test "returns an error if the given process is already a client" do
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, self()}
|
|
])
|
|
|
|
operation = {:client_join, self()}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "adds the given process to the client list" do
|
|
client_pid = self()
|
|
data = Data.new()
|
|
|
|
operation = {:client_join, client_pid}
|
|
assert {:ok, %{client_pids: [^client_pid]}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "adds new entry to the cell revisions map for the client with the latest revision" do
|
|
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
|
delta1 = Delta.new() |> Delta.insert("cats")
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client1_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
|
])
|
|
|
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
|
operation = {:client_join, client2_pid}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c1" => %{revision_by_client_pid: %{^client2_pid => 1}}}
|
|
}, _} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :client_leave" do
|
|
test "returns an error if the given process is not a client" do
|
|
data = Data.new()
|
|
|
|
operation = {:client_leave, self()}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "removes the given process from the client list" do
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, self()}
|
|
])
|
|
|
|
operation = {:client_leave, self()}
|
|
assert {:ok, %{client_pids: []}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "removes an entry in the the cell revisions map for the client and purges deltas" do
|
|
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
|
|
|
delta1 = Delta.new() |> Delta.insert("cats")
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client1_pid},
|
|
{:client_join, client2_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
|
])
|
|
|
|
operation = {:client_leave, client2_pid}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c1" => %{deltas: [], revision_by_client_pid: revision_by_client_pid}
|
|
}
|
|
}, _} = Data.apply_operation(data, operation)
|
|
|
|
assert revision_by_client_pid == %{client1_pid => 1}
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :apply_cell_delta" do
|
|
test "returns an error given invalid cell id" do
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, self()}
|
|
])
|
|
|
|
operation = {:apply_cell_delta, self(), "nonexistent", Delta.new(), 1}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error given non-joined client pid" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
delta = Delta.new() |> Delta.insert("cats")
|
|
operation = {:apply_cell_delta, self(), "c1", delta, 1}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error given invalid revision" do
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, self()},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
delta = Delta.new() |> Delta.insert("cats")
|
|
operation = {:apply_cell_delta, self(), "c1", delta, 5}
|
|
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "updates cell source according to the given delta" do
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, self()},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
delta = Delta.new() |> Delta.insert("cats")
|
|
operation = {:apply_cell_delta, self(), "c1", delta, 1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{cells: [%{source: "cats"}]}
|
|
]
|
|
},
|
|
cell_infos: %{"c1" => %{revision: 1}}
|
|
}, _actions} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "transforms the delta if the revision is not the most recent" do
|
|
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
|
|
|
delta1 = Delta.new() |> Delta.insert("cats")
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client1_pid},
|
|
{:client_join, client2_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
|
])
|
|
|
|
delta2 = Delta.new() |> Delta.insert("tea")
|
|
operation = {:apply_cell_delta, client2_pid, "c1", delta2, 1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
notebook: %{
|
|
sections: [
|
|
%{cells: [%{source: "catstea"}]}
|
|
]
|
|
},
|
|
cell_infos: %{"c1" => %{revision: 2}}
|
|
}, _} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns broadcast delta action with the transformed delta" do
|
|
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
|
|
|
delta1 = Delta.new() |> Delta.insert("cats")
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client1_pid},
|
|
{:client_join, client2_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
|
])
|
|
|
|
delta2 = Delta.new() |> Delta.insert("tea")
|
|
operation = {:apply_cell_delta, client2_pid, "c1", delta2, 1}
|
|
|
|
transformed_delta2 = Delta.new() |> Delta.retain(4) |> Delta.insert("tea")
|
|
|
|
assert {:ok, _data, [{:broadcast_delta, ^client2_pid, _cell, ^transformed_delta2}]} =
|
|
Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "given single client, does not keep deltas" do
|
|
client_pid = self()
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
delta = Delta.new() |> Delta.insert("cats")
|
|
operation = {:apply_cell_delta, client_pid, "c1", delta, 1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c1" => %{deltas: []}}
|
|
}, _} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "given multiple client, keeps the delta" do
|
|
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client1_pid},
|
|
{:client_join, client2_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
delta = Delta.new() |> Delta.insert("cats")
|
|
operation = {:apply_cell_delta, client1_pid, "c1", delta, 1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{"c1" => %{deltas: [^delta]}}
|
|
}, _} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :report_cell_revision" do
|
|
test "returns an error given invalid cell id" do
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, self()}
|
|
])
|
|
|
|
operation = {:report_cell_revision, self(), "nonexistent", 1}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error given non-joined client pid" do
|
|
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client1_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:apply_cell_delta, client1_pid, "c1", Delta.new(insert: "cats"), 1}
|
|
])
|
|
|
|
operation = {:report_cell_revision, client2_pid, "c1", 1}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "returns an error given invalid revision" do
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, self()},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"}
|
|
])
|
|
|
|
operation = {:report_cell_revision, self(), "c1", 1}
|
|
assert :error = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "updates client entry in the revisions map and purges unnecessary deltas" do
|
|
client1_pid = IEx.Helpers.pid(0, 0, 0)
|
|
client2_pid = IEx.Helpers.pid(0, 0, 1)
|
|
|
|
delta1 = Delta.new() |> Delta.insert("cats")
|
|
|
|
data =
|
|
data_after_operations!([
|
|
{:client_join, client1_pid},
|
|
{:client_join, client2_pid},
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:apply_cell_delta, client1_pid, "c1", delta1, 1}
|
|
])
|
|
|
|
operation = {:report_cell_revision, client2_pid, "c1", 1}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c1" => %{
|
|
deltas: [],
|
|
revision_by_client_pid: %{^client1_pid => 1, ^client2_pid => 1}
|
|
}
|
|
}
|
|
}, _} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :set_runtime" do
|
|
test "updates data with the given runtime" do
|
|
data = Data.new()
|
|
|
|
{:ok, runtime} = LiveBookTest.Runtime.SingleEvaluator.init()
|
|
|
|
operation = {:set_runtime, runtime}
|
|
|
|
assert {:ok, %{runtime: ^runtime}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
|
|
test "clears all statuses and the per-section queues" do
|
|
data =
|
|
data_after_operations!([
|
|
# First section with evaluating and queued cells
|
|
{:insert_section, 0, "s1"},
|
|
{:insert_cell, "s1", 0, :elixir, "c1"},
|
|
{:insert_cell, "s1", 1, :elixir, "c2"},
|
|
{:queue_cell_evaluation, "c1"},
|
|
{:queue_cell_evaluation, "c2"},
|
|
# Second section with evaluating and queued cells
|
|
{:insert_section, 1, "s2"},
|
|
{:insert_cell, "s2", 0, :elixir, "c3"},
|
|
{:insert_cell, "s2", 1, :elixir, "c4"},
|
|
{:queue_cell_evaluation, "c3"},
|
|
{:queue_cell_evaluation, "c4"}
|
|
])
|
|
|
|
{:ok, runtime} = LiveBookTest.Runtime.SingleEvaluator.init()
|
|
|
|
operation = {:set_runtime, runtime}
|
|
|
|
assert {:ok,
|
|
%{
|
|
cell_infos: %{
|
|
"c1" => %{validity_status: :fresh, evaluation_status: :ready},
|
|
"c2" => %{validity_status: :fresh, evaluation_status: :ready},
|
|
"c3" => %{validity_status: :fresh, evaluation_status: :ready},
|
|
"c4" => %{validity_status: :fresh, evaluation_status: :ready}
|
|
},
|
|
section_infos: %{
|
|
"s2" => %{evaluating_cell_id: nil, evaluation_queue: []},
|
|
"s1" => %{evaluating_cell_id: nil, evaluation_queue: []}
|
|
}
|
|
}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :set_path" do
|
|
test "updates data with the given path" do
|
|
data = Data.new()
|
|
operation = {:set_path, "path"}
|
|
|
|
assert {:ok, %{path: "path"}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
describe "apply_operation/2 given :mark_as_not_dirty" do
|
|
test "sets dirty flag to false" do
|
|
data =
|
|
data_after_operations!([
|
|
{:insert_section, 0, "s1"}
|
|
])
|
|
|
|
operation = :mark_as_not_dirty
|
|
|
|
assert {:ok, %{dirty: false}, []} = Data.apply_operation(data, operation)
|
|
end
|
|
end
|
|
|
|
defp data_after_operations!(operations) do
|
|
Enum.reduce(operations, Data.new(), fn operation, data ->
|
|
case Data.apply_operation(data, operation) do
|
|
{:ok, data, _action} ->
|
|
data
|
|
|
|
:error ->
|
|
raise "failed to set up test data, operation #{inspect(operation)} returned an error"
|
|
end
|
|
end)
|
|
end
|
|
end
|