defmodule ExshomeTest.TestMpvServer do
@moduledoc """
Test MPV server. You can use it to emulate an MPV server.
"""
use GenServer
alias ExUnit.Callbacks
import ExUnit.Assertions
alias ExshomeTest.Fixtures
import ExshomeTest.TestHelpers, only: [assert_receive_dependency: 1]
alias ExshomePlayer.Services.{MpvServer, MpvSocket}
defmodule State do
@moduledoc """
A structure to represent internal state of test MPV server.
"""
defstruct [
:server,
:connection,
:response_fn,
observed_properties: MapSet.new(),
received_messages: [],
playlist: []
]
@type t() :: %__MODULE__{
connection: :gen_tcp.socket() | nil,
received_messages: [term()],
server: :gen_tcp.socket() | nil,
observed_properties: MapSet.t(),
response_fn: ExshomeTest.TestMpvServer.response_fn() | nil,
playlist: [String.t()]
}
end
defmodule Arguments do
@moduledoc """
Arguments to start a test MPV server.
"""
@enforce_keys [:init_fn]
defstruct [:init_fn]
@type t() :: %__MODULE__{init_fn: (() -> any())}
end
@type response_fn() :: (request_id :: String.t(), data :: map() -> map())
@spec generate_random_tracks(Range.t()) :: :ok
def generate_random_tracks(amount \\ 1..10) do
amount = Enum.random(amount)
for _ <- 1..amount do
file_name = "track_#{Fixtures.unique_integer()}.mp3"
:ok =
MpvServer.music_folder()
|> Path.join(file_name)
|> File.touch!()
end
:ok
end
@spec received_messages() :: [map()]
def received_messages do
received_messages(test_server())
end
@spec received_messages(pid()) :: [map()]
def received_messages(server) do
GenServer.call(server, :messages)
end
@spec last_received_message() :: map()
def last_received_message do
[message | _] = received_messages()
message
end
@spec test_server() :: pid()
defp test_server do
Process.get(TestMpvServer) || raise "Test server not found"
end
@spec set_test_server(pid()) :: term()
defp set_test_server(server) do
Process.put(TestMpvServer, server)
end
@spec set_events([%{}]) :: term()
defp set_events(events) do
Process.put(:events, events)
end
def respond_with_errors do
set_response_fn(fn request_id, _ ->
%{request_id: request_id, error: "some error #{Fixtures.unique_integer()}"}
end)
end
@spec set_response_fn(function :: response_fn()) :: term()
def set_response_fn(function) do
set_response_fn(test_server(), function)
end
@spec set_response_fn(server :: pid(), response_fn :: response_fn()) :: term()
def set_response_fn(server, response_fn) do
GenServer.call(server, {:set_response_fn, response_fn})
end
@spec server_fixture() :: term()
def server_fixture do
my_pid = self()
server =
Callbacks.start_supervised!({
__MODULE__,
%Arguments{
init_fn: fn -> ExshomeTest.TestRegistry.allow(my_pid, self()) end
}
})
set_events([])
set_test_server(server)
server
end
@spec stop_server() :: :ok
def stop_server do
:ok = Callbacks.stop_supervised!(ExshomeTest.TestMpvServer)
end
@spec start_link(Arguments.t()) :: GenServer.on_start()
def start_link(%Arguments{} = init_arguments) do
GenServer.start_link(__MODULE__, init_arguments)
end
@spec send_event(map()) :: map()
def send_event(event) do
send_event(test_server(), event)
end
@spec send_event(server :: pid(), event :: %{}) :: term()
def send_event(server, event) do
GenServer.call(server, {:event, event})
end
@spec playlist() :: list(String.t())
def playlist do
GenServer.call(test_server(), :get_playlist)
end
def wait_until_socket_disconnects do
assert_receive_dependency({MpvSocket, :disconnected})
end
def wait_until_socket_connects do
assert_receive_dependency({MpvSocket, :connected})
end
@impl GenServer
def init(%Arguments{init_fn: init_fn}) do
init_fn.()
socket_path = MpvServer.socket_path()
File.rm(socket_path)
{:ok, server} =
:gen_tcp.listen(0, [
{:ip, {:local, socket_path}},
:binary,
{:packet, :line},
reuseaddr: true
])
state = %State{server: server}
{:ok, state, {:continue, :accept_connection}}
end
@impl GenServer
def handle_continue(:accept_connection, %State{} = state) do
{:ok, connection} = :gen_tcp.accept(state.server)
new_state = %State{state | connection: connection}
{:noreply, new_state}
end
@impl GenServer
def handle_info({:tcp, port, message}, %State{} = state) when port == state.connection do
decoded = Jason.decode!(message)
new_state = Map.update!(state, :received_messages, &[decoded | &1])
request_id = decoded["request_id"]
if new_state.response_fn do
response = state.response_fn.(request_id, decoded)
send_data(state, response)
{:noreply, new_state}
else
default_response_handler(request_id, decoded, new_state)
end
end
@impl GenServer
def handle_call(:messages, _from, %State{} = state) do
{:reply, state.received_messages, state}
end
@impl GenServer
def handle_call(:get_playlist, _from, %State{} = state) do
{:reply, state.playlist, state}
end
@impl GenServer
def handle_call({:event, event}, _from, %State{} = state) do
send_client_event(state, event)
{:reply, :ok, state}
end
@impl GenServer
def handle_call({:set_response_fn, response_fn}, _from, %State{} = state) do
new_state = %State{state | response_fn: response_fn}
{:reply, :ok, new_state}
end
@spec default_response_handler(
request_id :: String.t(),
request_data :: map(),
state :: State.t()
) :: {:noreply, State.t()}
def default_response_handler(
request_id,
%{"command" => [command_name | args]},
%State{} = state
) do
new_state = handle_command(command_name, args, state)
send_data(new_state, %{request_id: request_id, error: "success"})
{:noreply, new_state}
end
def default_response_handler(request_id, _request_data, %State{} = state) do
send_data(state, %{test: 123, request_id: request_id, error: "success"})
{:noreply, state}
end
@spec handle_command(command_name :: String.t(), args :: list(), state :: State.t()) ::
State.t()
defp handle_command("observe_property", [_subscription_id, property_name], %State{} = state) do
%State{
state
| observed_properties: MapSet.put(state.observed_properties, property_name)
}
|> update_property(property_name, nil)
end
defp handle_command("loadfile", [path], %State{} = state) do
%State{
state
| playlist: [path | state.playlist]
}
|> update_property("path", path)
end
defp handle_command("playlist-clear", [], %State{} = state) do
%State{state | playlist: []}
end
defp handle_command("set_property", [property_name, value], %State{} = state) do
update_property(state, property_name, value)
end
defp handle_command("seek", [value, "absolute"], %State{} = state) do
update_property(state, "time-pos", value)
end
defp handle_command("stop", [], %State{} = state) do
for property <- state.observed_properties, reduce: %State{state | playlist: []} do
state -> update_property(state, property, nil)
end
end
@spec update_property(state :: State.t(), property_name :: String.t(), value :: term()) ::
State.t()
defp update_property(%State{} = state, property_name, value) do
if MapSet.member?(state.observed_properties, property_name) do
send_client_event(state, %{event: "property-change", name: property_name, data: value})
end
state
end
defp send_client_event(%State{} = state, event) do
send_data(state, event)
end
defp send_data(%State{} = state, data) do
json_data = Jason.encode!(data)
:gen_tcp.send(state.connection, "#{json_data}\n")
end
end