Skip to content

Commit a423e25

Browse files
committed
chore: add tests
1 parent 5f32c6a commit a423e25

File tree

18 files changed

+481
-16
lines changed

18 files changed

+481
-16
lines changed

apps/expert/lib/expert/configuration.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ defmodule Expert.Configuration do
2626

2727
@dialyzer {:nowarn_function, set_dialyzer_enabled: 2}
2828

29-
@spec new(map(), String.t() | nil) :: t
29+
@spec new(Structures.ClientCapabilities.t(), String.t() | nil) :: t
3030
def new(%Structures.ClientCapabilities{} = client_capabilities, client_name) do
3131
support = Support.new(client_capabilities)
3232

apps/expert/lib/expert/project/supervisor.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ defmodule Expert.Project.Supervisor do
3939
DynamicSupervisor.terminate_child(Expert.Project.DynamicSupervisor.name(), pid)
4040
end
4141

42-
defp name(%Project{} = project) do
42+
def name(%Project{} = project) do
4343
:"#{Project.name(project)}::supervisor"
4444
end
4545
end

apps/expert/lib/expert/state.ex

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ defmodule Expert.State do
5151
_ -> nil
5252
end
5353

54-
root_path = Forge.Document.Path.from_uri(event.root_uri)
54+
root_path = Document.Path.from_uri(event.root_uri)
5555

5656
root_path
5757
|> Forge.Workspace.new()
@@ -126,8 +126,19 @@ defmodule Expert.State do
126126
removed_projects =
127127
for %{uri: uri} <- removed do
128128
project = Project.new(uri)
129-
Logger.info("Stopping project at uri #{uri}")
129+
130130
Expert.Project.Supervisor.stop(project)
131+
132+
GenLSP.notify(
133+
Expert.get_lsp(),
134+
%GenLSP.Notifications.WindowLogMessage{
135+
params: %GenLSP.Structures.LogMessageParams{
136+
type: GenLSP.Enumerations.MessageType.info(),
137+
message: "Stopping project node for #{Project.name(project)}"
138+
}
139+
}
140+
)
141+
131142
project
132143
end
133144

@@ -138,10 +149,8 @@ defmodule Expert.State do
138149
project
139150
end
140151

141-
projects =
142-
Enum.uniq((added_projects ++ ActiveProjects.projects()) -- removed_projects)
143-
144-
ActiveProjects.set_projects(projects)
152+
ActiveProjects.add_projects(added_projects)
153+
ActiveProjects.remove_projects(removed_projects)
145154

146155
state = %__MODULE__{state | workspace_folders: workspace_folders}
147156

@@ -188,12 +197,8 @@ defmodule Expert.State do
188197
config = state.configuration
189198

190199
project =
191-
case Enum.find(ActiveProjects.projects(), &Project.within_project?(&1, uri)) do
192-
nil ->
193-
Project.find_project(uri)
194-
195-
project ->
196-
project
200+
with nil <- Enum.find(ActiveProjects.projects(), &Project.within_project?(&1, uri)) do
201+
Project.find_project(uri)
197202
end
198203

199204
if project do
@@ -203,11 +208,10 @@ defmodule Expert.State do
203208

204209
case Document.Store.open(uri, text, version, language_id) do
205210
:ok ->
206-
Logger.info("################### opened #{uri}")
207211
{:ok, %{state | configuration: config}}
208212

209213
error ->
210-
Logger.error("################## Could not open #{uri} #{inspect(error)}")
214+
Logger.error("Could not open #{uri} #{inspect(error)}")
211215
error
212216
end
213217
end
@@ -274,13 +278,35 @@ defmodule Expert.State do
274278
{:ok, _pid} ->
275279
Logger.info("Project node started for #{Project.name(project)}")
276280

281+
GenLSP.notify(
282+
Expert.get_lsp(),
283+
%GenLSP.Notifications.WindowLogMessage{
284+
params: %GenLSP.Structures.LogMessageParams{
285+
type: GenLSP.Enumerations.MessageType.info(),
286+
message: "Started project node for #{Project.name(project)}"
287+
}
288+
}
289+
)
290+
277291
{:error, {reason, pid}} when reason in [:already_started, :already_present] ->
278292
{:ok, pid}
279293

280294
{:error, reason} ->
281295
Logger.error(
282296
"Failed to start project node for #{Project.name(project)}: #{inspect(reason, pretty: true)}"
283297
)
298+
299+
GenLSP.notify(
300+
Expert.get_lsp(),
301+
%GenLSP.Notifications.WindowLogMessage{
302+
params: %GenLSP.Structures.LogMessageParams{
303+
type: GenLSP.Enumerations.MessageType.error(),
304+
message: "Failed to start project node for #{Project.name(project)}"
305+
}
306+
}
307+
)
308+
309+
{:error, reason}
284310
end
285311
end
286312

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
defmodule ExpertTest do
2+
alias Forge.Document
3+
alias Forge.Project
4+
5+
import GenLSP.Test
6+
import Forge.Test.Fixtures
7+
8+
use ExUnit.Case, async: false
9+
10+
setup_all do
11+
start_supervised!({Document.Store, derive: [analysis: &Forge.Ast.analyze/1]})
12+
13+
project_root = fixtures_path() |> Path.join("workspace_folders")
14+
15+
main_project =
16+
project_root
17+
|> Path.join("main")
18+
|> Document.Path.to_uri()
19+
|> Project.new()
20+
21+
secondary_project =
22+
project_root
23+
|> Path.join("secondary")
24+
|> Document.Path.to_uri()
25+
|> Project.new()
26+
27+
[project_root: project_root, main_project: main_project, secondary_project: secondary_project]
28+
end
29+
30+
setup do
31+
start_supervised!({Task.Supervisor, name: :expert_task_queue})
32+
start_supervised!({DynamicSupervisor, name: Expert.DynamicSupervisor})
33+
start_supervised!({DynamicSupervisor, Expert.Project.DynamicSupervisor.options()})
34+
start_supervised!({Expert.ActiveProjects, []})
35+
36+
server =
37+
server(Expert,
38+
task_supervisor: :expert_task_queue,
39+
dynamic_supervisor: Expert.DynamicSupervisor
40+
)
41+
42+
Process.link(server.lsp)
43+
44+
client = client(server)
45+
46+
[server: server, client: client]
47+
end
48+
49+
def initialize_request(root_path, opts \\ []) do
50+
id = opts[:id] || 1
51+
projects = opts[:projects] || []
52+
53+
%{
54+
method: "initialize",
55+
id: id,
56+
jsonrpc: "2.0",
57+
params: %{
58+
rootUri: Document.Path.to_uri(root_path),
59+
initializationOptions: %{},
60+
capabilities: %{
61+
workspace: %{
62+
workspaceFolders: true
63+
}
64+
},
65+
workspaceFolders:
66+
Enum.map(projects, fn project ->
67+
%{uri: project.root_uri, name: Project.name(project)}
68+
end)
69+
}
70+
}
71+
end
72+
73+
def project_alive?(project) do
74+
project |> Expert.Project.Supervisor.name() |> Process.whereis() |> is_pid()
75+
end
76+
77+
describe "initialize request" do
78+
test "starts a project at the initial workspace folders", %{
79+
client: client,
80+
project_root: project_root,
81+
main_project: main_project
82+
} do
83+
assert :ok =
84+
request(
85+
client,
86+
initialize_request(project_root, id: 1, projects: [main_project])
87+
)
88+
89+
assert_result(1, %{
90+
"capabilities" => %{"workspace" => %{"workspaceFolders" => %{"supported" => true}}}
91+
})
92+
93+
expected_message = "Started project node for #{Project.name(main_project)}"
94+
95+
assert_notification(
96+
"window/logMessage",
97+
%{
98+
"type" => 3,
99+
"message" => ^expected_message
100+
}
101+
)
102+
103+
assert [project] = Expert.ActiveProjects.projects()
104+
assert project.root_uri == main_project.root_uri
105+
106+
assert project_alive?(main_project)
107+
end
108+
end
109+
110+
describe "workspace folders" do
111+
test "starts project nodes when adding workspace folders", %{
112+
client: client,
113+
project_root: project_root,
114+
main_project: main_project,
115+
secondary_project: secondary_project
116+
} do
117+
assert :ok =
118+
request(
119+
client,
120+
initialize_request(project_root, id: 1, projects: [main_project])
121+
)
122+
123+
assert_result(1, _)
124+
125+
expected_message = "Started project node for #{Project.name(main_project)}"
126+
127+
assert_notification(
128+
"window/logMessage",
129+
%{
130+
"type" => 3,
131+
"message" => ^expected_message
132+
}
133+
)
134+
135+
assert [_project_1] = Expert.ActiveProjects.projects()
136+
137+
assert :ok =
138+
notify(
139+
client,
140+
%{
141+
method: "workspace/didChangeWorkspaceFolders",
142+
jsonrpc: "2.0",
143+
params: %{
144+
event: %{
145+
added: [
146+
%{uri: secondary_project.root_uri, name: secondary_project.root_uri}
147+
],
148+
removed: []
149+
}
150+
}
151+
}
152+
)
153+
154+
expected_message = "Started project node for #{Project.name(secondary_project)}"
155+
156+
assert_notification(
157+
"window/logMessage",
158+
%{
159+
"type" => 3,
160+
"message" => ^expected_message
161+
}
162+
)
163+
164+
assert [project_1, project_2] = Expert.ActiveProjects.projects()
165+
assert project_1.root_uri == main_project.root_uri
166+
assert project_alive?(main_project)
167+
168+
assert project_2.root_uri == secondary_project.root_uri
169+
assert project_alive?(secondary_project)
170+
end
171+
172+
test "can remove workspace folders", %{
173+
client: client,
174+
project_root: project_root,
175+
main_project: main_project
176+
} do
177+
assert :ok =
178+
request(
179+
client,
180+
initialize_request(project_root, id: 1, projects: [main_project])
181+
)
182+
183+
assert_result(1, _)
184+
expected_message = "Started project node for #{Project.name(main_project)}"
185+
186+
assert_notification(
187+
"window/logMessage",
188+
%{
189+
"type" => 3,
190+
"message" => ^expected_message
191+
}
192+
)
193+
194+
assert [project] = Expert.ActiveProjects.projects()
195+
assert project.root_uri == main_project.root_uri
196+
assert project_alive?(main_project)
197+
198+
assert :ok =
199+
notify(
200+
client,
201+
%{
202+
method: "workspace/didChangeWorkspaceFolders",
203+
jsonrpc: "2.0",
204+
params: %{
205+
event: %{
206+
added: [],
207+
removed: [
208+
%{uri: main_project.root_uri, name: main_project.root_uri}
209+
]
210+
}
211+
}
212+
}
213+
)
214+
215+
expected_message = "Stopping project node for #{Project.name(main_project)}"
216+
217+
assert_notification(
218+
"window/logMessage",
219+
%{
220+
"type" => 3,
221+
"message" => ^expected_message
222+
}
223+
)
224+
225+
assert [] = Expert.ActiveProjects.projects()
226+
refute project_alive?(main_project)
227+
end
228+
end
229+
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez
18+
19+
# Ignore package tarball (built via "mix hex.build").
20+
main-*.tar
21+
22+
# Temporary files, for example, from tests.
23+
/tmp/

0 commit comments

Comments
 (0)