mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-10 15:04:25 +08:00
Add session and data tests
This commit is contained in:
parent
194814c4dc
commit
ba6506a55e
5 changed files with 130 additions and 11 deletions
|
@ -5,6 +5,20 @@
|
|||
*
|
||||
* This class takes `serverAdapter` and `editorAdapter` objects
|
||||
* that encapsulate the logic relevant for each part.
|
||||
*
|
||||
* ## Changes synchronization
|
||||
*
|
||||
* When the local editor emits a change (represented as delta),
|
||||
* the client sends this delta to the server and waits for an acknowledgement.
|
||||
* Until the acknowledgement comes, the client keeps all further
|
||||
* edits in buffer.
|
||||
* The server may send either an acknowledgement or other client's delta.
|
||||
* It's important to note that those messages come in what the server
|
||||
* believes is chronological order, so any delta received before
|
||||
* the acknowledgement should be treated as if it happened before
|
||||
* our unacknowledged delta.
|
||||
* Other client's delta is transformed against the local unacknowledged
|
||||
* deltas and applied to the editor.
|
||||
*/
|
||||
export default class EditorClient {
|
||||
constructor(serverAdapter, editorAdapter, revision) {
|
||||
|
|
|
@ -252,11 +252,15 @@ defmodule LiveBook.Session.Data do
|
|||
end
|
||||
|
||||
def apply_operation(data, {:apply_cell_delta, from, cell_id, delta, revision}) do
|
||||
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id) do
|
||||
with {:ok, cell, _} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
||||
cell_info <- data.cell_infos[cell.id],
|
||||
true <- 0 < revision and revision <= cell_info.revision + 1 do
|
||||
data
|
||||
|> with_actions()
|
||||
|> apply_delta(from, cell, delta, revision)
|
||||
|> wrap_ok()
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -429,20 +433,20 @@ defmodule LiveBook.Session.Data do
|
|||
|
||||
deltas_ahead = Enum.take(info.deltas, -(info.revision - revision + 1))
|
||||
|
||||
rebased_new_delta =
|
||||
Enum.reduce(deltas_ahead, delta, fn delta_ahead, rebased_new_delta ->
|
||||
Delta.transform(delta_ahead, rebased_new_delta, :left)
|
||||
transformed_new_delta =
|
||||
Enum.reduce(deltas_ahead, delta, fn delta_ahead, transformed_new_delta ->
|
||||
Delta.transform(delta_ahead, transformed_new_delta, :left)
|
||||
end)
|
||||
|
||||
new_source = Delta.apply_to_string(rebased_new_delta, cell.source)
|
||||
new_source = Delta.apply_to_string(transformed_new_delta, cell.source)
|
||||
|
||||
data_actions
|
||||
|> set!(notebook: Notebook.update_cell(data.notebook, cell.id, &%{&1 | source: new_source}))
|
||||
|> set_cell_info!(cell.id,
|
||||
deltas: info.deltas ++ [rebased_new_delta],
|
||||
deltas: info.deltas ++ [transformed_new_delta],
|
||||
revision: info.revision + 1
|
||||
)
|
||||
|> add_action({:broadcast_delta, from, %{cell | source: new_source}, rebased_new_delta})
|
||||
|> add_action({:broadcast_delta, from, %{cell | source: new_source}, transformed_new_delta})
|
||||
end
|
||||
|
||||
defp add_action({data, actions}, action) do
|
||||
|
|
3
mix.exs
3
mix.exs
|
@ -41,8 +41,7 @@ defmodule LiveBook.MixProject do
|
|||
{:telemetry_metrics, "~> 0.4"},
|
||||
{:telemetry_poller, "~> 0.4"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:text_delta, "~> 1.1"}
|
||||
{:plug_cowboy, "~> 2.0"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule LiveBook.Session.DataTest do
|
|||
use ExUnit.Case, async: true
|
||||
|
||||
alias LiveBook.Session.Data
|
||||
alias LiveBook.Delta
|
||||
|
||||
describe "apply_operation/2 given :insert_section" do
|
||||
test "adds new section to notebook and session info" do
|
||||
|
@ -478,6 +479,92 @@ defmodule LiveBook.Session.DataTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "apply_operation/2 given :apply_cell_delta" do
|
||||
test "returns an error given invalid cell id" do
|
||||
data = Data.new()
|
||||
operation = {:apply_cell_delta, self(), "nonexistent", Delta.new(), 1}
|
||||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "returns an error given invalid revision" 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, 5}
|
||||
|
||||
assert :error = Data.apply_operation(data, operation)
|
||||
end
|
||||
|
||||
test "updates cell source according to the given delta" 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 {: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
|
||||
delta1 = Delta.new() |> Delta.insert("cats")
|
||||
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, 0, "s1"},
|
||||
{:insert_cell, "s1", 0, :elixir, "c1"},
|
||||
{:apply_cell_delta, self(), "c1", delta1, 1}
|
||||
])
|
||||
|
||||
delta2 = Delta.new() |> Delta.insert("tea")
|
||||
operation = {:apply_cell_delta, self(), "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
|
||||
delta1 = Delta.new() |> Delta.insert("cats")
|
||||
|
||||
data =
|
||||
data_after_operations!([
|
||||
{:insert_section, 0, "s1"},
|
||||
{:insert_cell, "s1", 0, :elixir, "c1"},
|
||||
{:apply_cell_delta, self(), "c1", delta1, 1}
|
||||
])
|
||||
|
||||
delta2 = Delta.new() |> Delta.insert("tea")
|
||||
operation = {:apply_cell_delta, self(), "c1", delta2, 1}
|
||||
|
||||
from = self()
|
||||
transformed_delta2 = Delta.new() |> Delta.retain(4) |> Delta.insert("tea")
|
||||
|
||||
assert {:ok, _data, [{:broadcast_delta, ^from, _cell, ^transformed_delta2}]} =
|
||||
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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule LiveBook.SessionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias LiveBook.Session
|
||||
alias LiveBook.{Session, Delta}
|
||||
|
||||
setup do
|
||||
{:ok, _} = Session.start_link("1")
|
||||
|
@ -100,7 +100,22 @@ defmodule LiveBook.SessionTest do
|
|||
{section_id, _cell_id} = insert_section_and_cell(session_id)
|
||||
|
||||
Session.set_section_name(session_id, section_id, "Chapter 1")
|
||||
assert_receive {:operation, {:set_section_name, section_id, "Chapter 1"}}
|
||||
assert_receive {:operation, {:set_section_name, ^section_id, "Chapter 1"}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "apply_cell_delta/5" do
|
||||
test "sends a cell delta operation to subscribers", %{session_id: session_id} do
|
||||
Phoenix.PubSub.subscribe(LiveBook.PubSub, "sessions:#{session_id}")
|
||||
|
||||
{_section_id, cell_id} = insert_section_and_cell(session_id)
|
||||
|
||||
from = self()
|
||||
delta = Delta.new() |> Delta.insert("cats")
|
||||
revision = 1
|
||||
|
||||
Session.apply_cell_delta(session_id, from, cell_id, delta, revision)
|
||||
assert_receive {:operation, {:apply_cell_delta, ^from, ^cell_id, ^delta, ^revision}}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue