Skip to content

Commit 157f04e

Browse files
committed
Convert Connection to :gen_statem
1 parent 7aec499 commit 157f04e

File tree

3 files changed

+83
-117
lines changed

3 files changed

+83
-117
lines changed
Lines changed: 81 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,47 @@
11
defmodule PhoenixTest.Playwright.Connection do
22
@moduledoc """
3-
Stateful, `GenServer` based connection to a Playwright node.js server.
4-
The connection is established via `PhoenixTest.Playwright.Port`.
3+
Stateful, `:gen_statem` based connection to a Playwright node.js server.
4+
The connection is established via `PhoenixTest.Playwright.PortServer`.
5+
6+
States:
7+
- `:pending`: Initial state, waiting for Playwright initialization. Post calls are postponed.
8+
- `:started`: Playwright is ready, all operations are processed normally.
59
610
You won't usually have to use this module directly.
711
`PhoenixTest.Playwright` uses this under the hood.
812
"""
9-
use GenServer
13+
@behaviour :gen_statem
1014

1115
alias PhoenixTest.Playwright.Config
1216
alias PhoenixTest.Playwright.PortServer
1317

1418
@timeout_grace_factor 1.5
1519
@min_genserver_timeout to_timeout(second: 1)
1620

17-
defstruct status: :pending,
18-
awaiting_started: [],
19-
initializers: %{},
21+
defstruct initializers: %{},
2022
guid_subscribers: %{},
2123
posts_in_flight: %{}
2224

2325
@name __MODULE__
2426

27+
@doc false
2528
def start_link do
26-
GenServer.start_link(__MODULE__, :no_init_arg, name: @name, timeout: Config.global(:timeout))
29+
:gen_statem.start_link({:local, @name}, __MODULE__, :no_init_arg, timeout: Config.global(:timeout))
2730
end
2831

2932
@doc """
3033
Launch a browser and return its `guid`.
3134
"""
3235
def launch_browser(type, opts) do
3336
ensure_started()
34-
3537
types = initializer("Playwright")
3638
type_id = Map.fetch!(types, type).guid
37-
38-
timeout =
39-
opts[:browser_launch_timeout] || opts[:timeout] || Config.global(:browser_launch_timeout)
40-
41-
params =
42-
opts
43-
|> Map.new()
44-
|> Map.put(:timeout, timeout)
39+
timeout = opts[:browser_launch_timeout] || opts[:timeout] || Config.global(:browser_launch_timeout)
40+
params = opts |> Map.new() |> Map.put(:timeout, timeout)
4541

4642
case post(guid: type_id, method: :launch, params: params) do
47-
%{result: %{browser: %{guid: guid}}} ->
48-
guid
49-
50-
%{error: %{error: %{name: "TimeoutError", stack: stack, message: message}}} ->
51-
raise """
52-
Timed out while launching the Playwright browser, #{String.capitalize("#{type}")}. #{message}
53-
54-
You may need to increase the :browser_launch_timeout option in config/test.exs:
55-
56-
config :phoenix_test,
57-
playwright: [
58-
browser_launch_timeout: 10_000,
59-
# other Playwright options...
60-
],
61-
# other phoenix_test options...
62-
63-
Playwright backtrace:
64-
65-
#{stack}
66-
"""
43+
%{result: %{browser: %{guid: guid}}} -> guid
44+
%{error: %{error: %{name: "TimeoutError"} = error}} -> raise launch_timeout_error_msg(type, error)
6745
end
6846
end
6947

@@ -72,152 +50,140 @@ defmodule PhoenixTest.Playwright.Connection do
7250
nil -> start_link()
7351
pid -> {:ok, pid}
7452
end
75-
76-
GenServer.call(@name, :awaiting_started)
7753
end
7854

7955
@doc """
8056
Subscribe to messages for a guid and its descendants.
8157
"""
8258
def subscribe(pid \\ self(), guid) do
83-
GenServer.cast(@name, {:subscribe, {pid, guid}})
59+
:gen_statem.cast(@name, {:subscribe, pid, guid})
8460
end
8561

86-
@doc """
87-
Handle a parsed message from the PortServer.
88-
This is called by PortServer after parsing complete messages from the Port.
89-
"""
62+
@doc false
9063
def handle_playwright_msg(msg) do
91-
GenServer.cast(@name, {:playwright_msg, msg})
64+
:gen_statem.cast(@name, {:msg, msg})
9265
end
9366

9467
@doc """
9568
Post a message and await the response.
96-
We wait for an additional grace period after the timeout that we pass to playwright.
69+
Wait for an additional grace period after the playwright timeout.
9770
"""
9871
def post(msg, timeout \\ nil) do
99-
default = %{params: %{}, metadata: %{}}
100-
10172
msg =
10273
msg
103-
|> Enum.into(default)
74+
|> Enum.into(%{params: %{}, metadata: %{}})
10475
|> update_in(~w(params timeout)a, &(&1 || timeout || Config.global(:timeout)))
76+
|> Map.put_new_lazy(:id, fn -> System.unique_integer([:positive, :monotonic]) end)
10577

10678
call_timeout = max(@min_genserver_timeout, round(msg.params.timeout * @timeout_grace_factor))
107-
GenServer.call(@name, {:post, msg}, call_timeout)
79+
80+
:gen_statem.call(@name, {:post, msg}, call_timeout)
10881
end
10982

11083
@doc """
11184
Get the initializer data for a channel.
11285
"""
11386
def initializer(guid) do
114-
GenServer.call(@name, {:initializer, guid})
87+
:gen_statem.call(@name, {:initializer, guid})
11588
end
11689

117-
@impl GenServer
90+
@impl :gen_statem
91+
def callback_mode, do: :state_functions
92+
93+
@impl :gen_statem
11894
def init(:no_init_arg) do
119-
{:ok, _} = PortServer.start_link(self())
95+
{:ok, _port_server} = PortServer.start_link(self())
12096
msg = %{guid: "", params: %{sdk_language: :javascript}, method: :initialize, metadata: %{}}
12197
PortServer.post(msg)
12298

123-
{:ok, %__MODULE__{}}
99+
{:ok, :pending, %__MODULE__{}}
124100
end
125101

126-
@impl GenServer
127-
def handle_cast({:subscribe, {recipient, guid}}, state) do
128-
subscribers = Map.update(state.guid_subscribers, guid, [recipient], &[recipient | &1])
129-
{:noreply, %{state | guid_subscribers: subscribers}}
102+
@doc false
103+
def pending(:cast, {:msg, %{method: :__create__, params: %{guid: "Playwright"}} = msg}, data) do
104+
{:next_state, :started, add_initializer(data, msg)}
130105
end
131106

132-
def handle_cast({:playwright_msg, msg}, state) do
133-
state = handle_recv(msg, state)
134-
{:noreply, state}
135-
end
107+
def pending(:cast, _msg, _data), do: {:keep_state_and_data, [:postpone]}
108+
def pending({:call, _from}, _msg, _data), do: {:keep_state_and_data, [:postpone]}
136109

137-
@impl GenServer
138-
def handle_call({:post, msg}, from, state) do
139-
msg_id = fn -> System.unique_integer([:positive, :monotonic]) end
140-
msg = msg |> Map.new() |> Map.put_new_lazy(:id, msg_id)
110+
@doc false
111+
def started({:call, from}, {:post, msg}, data) do
141112
PortServer.post(msg)
142-
143-
{:noreply, Map.update!(state, :posts_in_flight, &Map.put(&1, msg.id, from))}
144-
end
145-
146-
def handle_call({:initializer, guid}, _from, state) do
147-
{:reply, Map.get(state.initializers, guid), state}
113+
{:keep_state, put_in(data.posts_in_flight[msg.id], from)}
148114
end
149115

150-
def handle_call(:awaiting_started, from, %{status: :pending} = state) do
151-
{:noreply, Map.update!(state, :awaiting_started, &[from | &1])}
116+
def started({:call, from}, {:initializer, guid}, data) do
117+
{:keep_state_and_data, [{:reply, from, Map.fetch!(data.initializers, guid)}]}
152118
end
153119

154-
def handle_call(:awaiting_started, _from, %{status: :started} = state) do
155-
{:reply, :ok, state}
120+
def started(:cast, {:subscribe, recipient, guid}, data) do
121+
{:keep_state, update_in(data.guid_subscribers[guid], &[recipient | &1 || []])}
156122
end
157123

158-
defp handle_recv(msg, state) do
159-
state
160-
|> log_js(msg)
161-
|> add_initializer(msg)
162-
|> handle_started(msg)
163-
|> reply_in_flight(msg)
164-
|> send_to_subscribers(msg)
165-
end
166-
167-
defp log_js(state, %{method: :page_error} = msg) do
124+
def started(:cast, {:msg, %{method: :page_error} = msg}, _data) do
168125
if module = Config.global(:js_logger) do
169126
module.log(:error, msg.params.error, msg)
170127
end
171128

172-
state
129+
:keep_state_and_data
173130
end
174131

175-
defp log_js(state, %{method: :console} = msg) do
132+
def started(:cast, {:msg, %{method: :console} = msg}, _data) do
176133
if module = Config.global(:js_logger) do
177-
level =
178-
case msg[:params][:type] do
179-
"error" -> :error
180-
"debug" -> :debug
181-
_ -> :info
182-
end
183-
134+
level = log_level_from_js(msg[:params][:type])
184135
module.log(level, msg.params.text, msg)
185136
end
186137

187-
state
138+
:keep_state_and_data
188139
end
189140

190-
defp log_js(state, _), do: state
141+
def started(:cast, {:msg, msg}, data) when is_map_key(data.posts_in_flight, msg.id) do
142+
{from, posts_in_flight} = Map.pop(data.posts_in_flight, msg.id)
143+
:gen_statem.reply(from, msg)
191144

192-
defp handle_started(state, %{method: :__create__, params: %{type: "Playwright"}}) do
193-
for from <- state.awaiting_started, do: GenServer.reply(from, :ok)
194-
%{state | status: :started, awaiting_started: :none}
145+
{:keep_state, %{data | posts_in_flight: posts_in_flight}}
195146
end
196147

197-
defp handle_started(state, _), do: state
198-
199-
defp add_initializer(state, %{method: :__create__} = msg) do
200-
Map.update!(state, :initializers, &Map.put(&1, msg.params.guid, msg.params.initializer))
148+
def started(:cast, {:msg, msg}, data) do
149+
{:keep_state, data |> add_initializer(msg) |> notify_subscribers(msg)}
201150
end
202151

203-
defp add_initializer(state, _), do: state
152+
defp add_initializer(data, %{method: :__create__} = msg) do
153+
put_in(data.initializers[msg.params.guid], msg.params.initializer)
154+
end
204155

205-
defp reply_in_flight(%{posts_in_flight: in_flight} = state, msg) when is_map_key(in_flight, msg.id) do
206-
{from, in_flight} = Map.pop(in_flight, msg.id)
207-
GenServer.reply(from, msg)
156+
defp add_initializer(data, _msg), do: data
208157

209-
%{state | posts_in_flight: in_flight}
158+
defp notify_subscribers(data, msg) when is_map_key(data.guid_subscribers, msg.guid) do
159+
for pid <- Map.fetch!(data.guid_subscribers, msg.guid), do: send(pid, {:playwright_msg, msg})
160+
data
210161
end
211162

212-
defp reply_in_flight(state, _), do: state
163+
defp notify_subscribers(data, _msg), do: data
213164

214-
defp send_to_subscribers(state, %{guid: guid} = msg) do
215-
for pid <- Map.get(state.guid_subscribers, guid, []) do
216-
send(pid, {:playwright, msg})
217-
end
165+
defp launch_timeout_error_msg(type, error) do
166+
%{stack: stack, message: message} = error
167+
168+
"""
169+
Timed out while launching the Playwright browser, #{String.capitalize("#{type}")}. #{message}
170+
171+
You may need to increase the :browser_launch_timeout option in config/test.exs:
172+
173+
config :phoenix_test,
174+
playwright: [
175+
browser_launch_timeout: 10_000,
176+
# other Playwright options...
177+
],
178+
# other phoenix_test options...
179+
180+
Playwright backtrace:
218181
219-
state
182+
#{stack}
183+
"""
220184
end
221185

222-
defp send_to_subscribers(state, _), do: state
186+
defp log_level_from_js("error"), do: :error
187+
defp log_level_from_js("debug"), do: :debug
188+
defp log_level_from_js(_), do: :info
223189
end

lib/phoenix_test/playwright/event_listener.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ defmodule PhoenixTest.Playwright.EventListener do
3939
end
4040

4141
@impl GenServer
42-
def handle_info({:playwright, event}, %__MODULE__{} = state) do
42+
def handle_info({:playwright_msg, event}, %__MODULE__{} = state) do
4343
callback = List.first(state.callbacks)
4444
if state.filter.(event) and callback, do: callback.(event)
4545
{:noreply, state}

lib/phoenix_test/playwright/event_recorder.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defmodule PhoenixTest.Playwright.EventRecorder do
2727
end
2828

2929
@impl GenServer
30-
def handle_info({:playwright, event}, state) do
30+
def handle_info({:playwright_msg, event}, state) do
3131
if state.filter.(event) do
3232
{:noreply, Map.update!(state, :events, &[event | &1])}
3333
else

0 commit comments

Comments
 (0)