diff --git a/.formatter.exs b/.formatter.exs index 161c83f..41c70f8 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -12,7 +12,7 @@ locals = [ [ locals_without_parens: locals, - import_deps: [:typed_struct], + import_deps: [:typed_struct, :stream_data], inputs: ["{mix,.formatter}.exs", "{config,lib,test,}/**/*.{ex,exs}"], export: [ locals_without_parens: locals diff --git a/lib/gen_lsp/communication/stdio.ex b/lib/gen_lsp/communication/stdio.ex index 50cdcbd..7626daf 100644 --- a/lib/gen_lsp/communication/stdio.ex +++ b/lib/gen_lsp/communication/stdio.ex @@ -5,8 +5,6 @@ defmodule GenLSP.Communication.Stdio do This is the default adapter, and is the communication channel that most LSP clients expect to be able to use. """ - require Logger - @behaviour GenLSP.Communication.Adapter @separator "\r\n\r\n" @@ -43,6 +41,9 @@ defmodule GenLSP.Communication.Stdio do :eof -> :eof + {:error, error} -> + {:error, error} + headers -> body = headers @@ -59,6 +60,9 @@ defmodule GenLSP.Communication.Stdio do :eof -> :eof + {:error, error} -> + {:error, error} + line -> line = String.trim(line) @@ -81,6 +85,9 @@ defmodule GenLSP.Communication.Stdio do :eof -> :eof + {:error, error} -> + {:error, error} + payload -> payload end diff --git a/mix.exs b/mix.exs index fa97c2f..6049413 100644 --- a/mix.exs +++ b/mix.exs @@ -40,6 +40,7 @@ defmodule GenLSP.MixProject do {:nimble_options, "~> 0.5 or ~> 1.0"}, # {:schematic, path: "../schematic"}, {:schematic, "~> 0.2.1"}, + {:stream_data, "~> 0.6.0"}, {:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index e460ab2..dc33102 100644 --- a/mix.lock +++ b/mix.lock @@ -10,6 +10,7 @@ "nimble_options": {:hex, :nimble_options, "1.0.1", "b448018287b22584e91b5fd9c6c0ad717cb4bcdaa457957c8d57770f56625c43", [:mix], [], "hexpm", "078b2927cd9f84555be6386d56e849b0c555025ecccf7afee00ab6a9e6f63837"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, + "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, } diff --git a/test/gen_lsp/communication/stdio_test.exs b/test/gen_lsp/communication/stdio_test.exs index fa1b0cb..1fc63c6 100644 --- a/test/gen_lsp/communication/stdio_test.exs +++ b/test/gen_lsp/communication/stdio_test.exs @@ -1,35 +1,54 @@ defmodule GenLSP.Communication.StdioTest do use ExUnit.Case, async: true + use ExUnitProperties + # this includes a char that is 3 bytes in length @string ~s|{"a":"‘"}| @length byte_size(@string) - @command "elixir --erl '-kernel standard_io_encoding latin1' -S mix run -e ' + @command "elixir --sname $shortname --cookie monster --erl '-kernel standard_io_encoding latin1' -S mix run -e ' defmodule GenLSP.Support.Buffer do - def loop do + def loop(pid) do case GenLSP.Communication.Stdio.read([], nil) do :eof -> + System.halt() :eof + {:error, _reason} = error -> + Process.send(pid, {:buffer, error}, []) + {:ok, body, _} -> - body - |> Jason.decode!() - |> Jason.encode!() - |> GenLSP.Communication.Stdio.write([]) + case body |> Jason.decode() do + {:ok, _} -> + Process.send(pid, {:buffer, :success}, []) - loop() + error -> + Process.send(pid, {:buffer, error, byte_size(body)}, []) + end + + loop(pid) end end end defmodule Main do def run() do + true = + Enum.reduce_while(0..20, false, fn _, _ -> + if Node.connect(:\"gen_lsp_test@nublar\") do + Process.sleep(250) + {:halt, true} + else + {:cont, false} + end + end) + pid = \"GENLSPPID\" |> System.get_env() |> Base.decode64!() |> :erlang.binary_to_term() GenLSP.Communication.Stdio.init([]) # the following match ensures that the script completes and does # not raise after stdin is closed. - :eof = GenLSP.Support.Buffer.loop() + GenLSP.Support.Buffer.loop(pid) end end @@ -47,6 +66,116 @@ Main.run()'" ) # assert the message is echoed back - assert_receive {^port, {:data, ^expected_message}}, 2000 + assert_receive {^port, {:data, ^expected_message}}, 15000 + end + + test "works" do + {:ok, _} = Node.start(:gen_lsp_test, :shortnames) + Node.set_cookie(:monster) + packets = "failed.bin" |> File.read!() |> :erlang.binary_to_term() + # dbg(packets) + + node = "stdio-test-#{System.system_time()}" + + port = + Port.open( + {:spawn, String.replace(@command, "$shortname", node)}, + [ + :binary, + line: 1024, + env: [ + {~c"MIX_ENV", ~c"test"}, + {~c"GENLSPPID", + :erlang.term_to_binary(self()) |> Base.encode64() |> String.to_charlist()} + ] + ] + ) + + for packet <- packets do + packet = Jason.encode!(packet) + + length = byte_size(packet) + + # send our message + assert Port.command( + port, + "Whoa: Buddy\nContent-Length: #{length}\nFoo: Bar\r\n\r\n#{packet}" + ) + end + + for _packet <- packets do + # assert the message is echoed back + assert_receive {:buffer, actual_message}, 10000, "failed to receive output" + + if actual_message != :success do + # File.write!("failed.bin", :erlang.term_to_binary(packets)) + Node.disconnect(:"#{node}") + Port.close(port) + flunk("output was incorrect, received: >>>\n#{inspect(actual_message)}\n<<<") + end + end + + Node.disconnect(:"#{node}") + Port.close(port) + end + + @tag timeout: :infinity + property "can read any kind of data through stdio" do + {:ok, _} = Node.start(:gen_lsp_test, :shortnames) + Node.set_cookie(:monster) + + simple_term = one_of([boolean(), integer(), string(:utf8)]) + + json = + tree(simple_term, fn leaf -> + one_of([list_of(leaf), map_of(string(:utf8), leaf)]) + end) + + check all packets <- list_of(json, min_length: 1), max_runs: 1000 do + node = "stdio-test-#{System.system_time()}" + + port = + Port.open( + {:spawn, String.replace(@command, "$shortname", node)}, + [ + :binary, + line: 1024, + env: [ + {~c"MIX_ENV", ~c"test"}, + {~c"GENLSPPID", + :erlang.term_to_binary(self()) |> Base.encode64() |> String.to_charlist()} + ] + ] + ) + + for packet <- packets do + packet = Jason.encode!(packet) + + length = byte_size(packet) + + # send our message + assert Port.command( + port, + "Whoa: Buddy\nContent-Length: #{length}\nFoo: Bar\r\n\r\n#{packet}" + ) + end + + for packet <- packets do + # assert the message is echoed back + assert_receive {:buffer, actual_message}, 10000, "failed to receive output" + + if actual_message != :success do + length = byte_size(packet) + dbg(length) + File.write!("failed.bin", :erlang.term_to_binary(packets)) + Node.disconnect(:"#{node}") + Port.close(port) + flunk("output was incorrect, received: >>>\n#{inspect(actual_message)}\n<<<") + end + end + + Node.disconnect(:"#{node}") + Port.close(port) + end end end