Skip to content

Commit 0f11f1a

Browse files
committed
wip!: fix tests for windows
1 parent 78ffdf3 commit 0f11f1a

File tree

15 files changed

+258
-124
lines changed

15 files changed

+258
-124
lines changed

apps/engine/lib/engine/mix.tasks.deps.safe_compile.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ unless Elixir.Features.compile_keeps_current_directory?() do
282282
makefile_win? = makefile_win?(dep)
283283

284284
command =
285-
case :os.type() do
285+
case Forge.OS.type() do
286286
{:win32, _} when makefile_win? ->
287287
"nmake /F Makefile.win"
288288

apps/engine/lib/engine/search/indexer.ex

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ defmodule Engine.Search.Indexer do
7070
with {:ok, contents} <- File.read(path),
7171
{:ok, entries} <- Indexer.Source.index(path, contents) do
7272
Enum.filter(entries, fn entry ->
73-
if contained_in?(path, deps_dir) do
73+
if Forge.Path.contains?(path, deps_dir) do
7474
entry.subtype == :definition
7575
else
7676
true
@@ -185,19 +185,15 @@ defmodule Engine.Search.Indexer do
185185
build_dir = build_dir()
186186

187187
[root_dir, "**", @indexable_extensions]
188-
|> Path.join()
189-
|> Path.wildcard()
190-
|> Enum.reject(&contained_in?(&1, build_dir))
188+
|> Forge.Path.glob()
189+
|> Enum.reject(&Forge.Path.contains?(&1, build_dir))
191190
end
192191

193192
# stat(path) is here for testing so it can be mocked
194193
defp stat(path) do
195194
File.stat(path)
196195
end
197196

198-
defp contained_in?(file_path, possible_parent) do
199-
String.starts_with?(file_path, possible_parent)
200-
end
201197

202198
defp deps_dir do
203199
case Engine.Mix.in_project(&Mix.Project.deps_path/0) do

apps/expert/lib/expert/engine_node.ex

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,8 @@ defmodule Expert.EngineNode do
181181

182182
def glob_paths(_) do
183183
entries =
184-
Mix.Project.build_path()
185-
|> Path.join("**/ebin")
186-
|> Path.wildcard()
184+
[Mix.Project.build_path(), "**/ebin"]
185+
|> Forge.Path.glob()
187186
|> Enum.filter(fn entry ->
188187
Enum.any?(@allowed_apps, &String.contains?(entry, to_string(&1)))
189188
end)
@@ -237,19 +236,17 @@ defmodule Expert.EngineNode do
237236
]
238237

239238
{launcher, opts} =
240-
case :os.type() do
241-
{:win32, _} ->
242-
{elixir, opts}
239+
if Forge.OS.windows? do
240+
{elixir, opts}
241+
else
242+
launcher = Expert.Port.path()
243243

244-
{:unix, _} ->
245-
launcher = Expert.Port.path()
244+
opts =
245+
Keyword.update(opts, :args, [elixir], fn old_args ->
246+
[elixir | Enum.map(old_args, &to_string/1)]
247+
end)
246248

247-
opts =
248-
Keyword.update(opts, :args, [elixir], fn old_args ->
249-
[elixir | Enum.map(old_args, &to_string/1)]
250-
end)
251-
252-
{launcher, opts}
249+
{launcher, opts}
253250
end
254251

255252
GenLSP.info(lsp, "Finding or building engine for project #{project_name}")
@@ -288,9 +285,8 @@ defmodule Expert.EngineNode do
288285
end
289286

290287
defp ebin_paths(base_path) do
291-
base_path
292-
|> Path.join("lib/**/ebin")
293-
|> Path.wildcard()
288+
[base_path, "lib/**/ebin"]
289+
|> Forge.Path.glob()
294290
end
295291
end
296292

@@ -315,7 +311,7 @@ defmodule Expert.EngineNode do
315311
GenServer.start_link(__MODULE__, state, name: name(project))
316312
end
317313

318-
@start_timeout 3_000
314+
@start_timeout 10_000
319315

320316
defp start_node(project, paths) do
321317
project

apps/expert/lib/expert/port.ex

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -36,49 +36,48 @@ defmodule Expert.Port do
3636
end
3737

3838
def elixir_executable(%Project{} = project) do
39-
case :os.type() do
40-
{:win32, _} ->
41-
# Remove the burrito binaries from PATH
42-
path =
43-
"PATH"
44-
|> System.get_env()
45-
|> String.split(";", parts: 2)
46-
|> List.last()
47-
48-
case :os.find_executable(~c"elixir", to_charlist(path)) do
49-
false ->
50-
{:error, :no_elixir, "Couldn't find an elixir executable"}
51-
52-
elixir ->
53-
env =
54-
Enum.map(System.get_env(), fn
55-
{"PATH", _path} -> {"PATH", path}
56-
other -> other
57-
end)
58-
59-
{:ok, elixir, env}
60-
end
61-
62-
_ ->
63-
root_path = Project.root_path(project)
64-
65-
shell = System.get_env("SHELL")
66-
path = path_env_at_directory(root_path, shell)
67-
68-
case :os.find_executable(~c"elixir", to_charlist(path)) do
69-
false ->
70-
{:error, :no_elixir,
71-
"Couldn't find an elixir executable for project at #{root_path}. Using shell at #{shell} with PATH=#{path}"}
72-
73-
elixir ->
74-
env =
75-
Enum.map(System.get_env(), fn
76-
{"PATH", _path} -> {"PATH", path}
77-
other -> other
78-
end)
79-
80-
{:ok, elixir, env}
81-
end
39+
if Forge.OS.windows? do
40+
# Remove the burrito binaries from PATH
41+
path =
42+
"PATH"
43+
|> System.get_env()
44+
|> String.split(";", parts: 2)
45+
|> List.last()
46+
47+
case :os.find_executable(~c"elixir", to_charlist(path)) do
48+
false ->
49+
{:error, :no_elixir, "Couldn't find an elixir executable"}
50+
51+
elixir ->
52+
env =
53+
Enum.map(System.get_env(), fn
54+
{"PATH", _path} -> {"PATH", path}
55+
other -> other
56+
end)
57+
58+
{:ok, elixir, env}
59+
end
60+
61+
else
62+
root_path = Project.root_path(project)
63+
64+
shell = System.get_env("SHELL")
65+
path = path_env_at_directory(root_path, shell)
66+
67+
case :os.find_executable(~c"elixir", to_charlist(path)) do
68+
false ->
69+
{:error, :no_elixir,
70+
"Couldn't find an elixir executable for project at #{root_path}. Using shell at #{shell} with PATH=#{path}"}
71+
72+
elixir ->
73+
env =
74+
Enum.map(System.get_env(), fn
75+
{"PATH", _path} -> {"PATH", path}
76+
other -> other
77+
end)
78+
79+
{:ok, elixir, env}
80+
end
8281
end
8382
end
8483

@@ -123,7 +122,7 @@ defmodule Expert.Port do
123122
Launches an executable in the project context via a port.
124123
"""
125124
def open(%Project{} = project, executable, opts) do
126-
{os_type, _} = :os.type()
125+
{os_type, _} = Forge.OS.type()
127126

128127
opts =
129128
opts
@@ -158,7 +157,7 @@ defmodule Expert.Port do
158157
Provides the path of an executable to launch another erlang node via ports.
159158
"""
160159
def path do
161-
path(:os.type())
160+
path(Forge.OS.type)
162161
end
163162

164163
def path({:unix, _}) do

apps/expert/lib/expert/provider/handlers/code_lens.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,17 @@ defmodule Expert.Provider.Handlers.CodeLens do
5353
end
5454

5555
defp show_reindex_lens?(%Project{} = project, %Document{} = document) do
56-
document_path = Path.expand(document.path)
56+
document_path = normalize_path(document.path)
57+
mix_exs_path = normalize_path(Project.mix_exs_path(project))
5758

58-
document_path == Project.mix_exs_path(project) and
59+
document_path == mix_exs_path and
5960
not EngineApi.index_running?(project)
6061
end
62+
63+
defp normalize_path(path) do
64+
path
65+
|> Path.expand()
66+
|> String.downcase()
67+
|> Forge.Path.normalize_for_glob()
68+
end
6169
end

apps/expert/test/engine/engine_test.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ defmodule EngineTest do
1717

1818
def engine_cwd(project) do
1919
EngineApi.call(project, File, :cwd!, [])
20+
|> normalize_path_separators()
21+
end
22+
23+
defp normalize_path_separators(path) when is_binary(path) do
24+
if Forge.OS.windows?() do
25+
String.replace(path, "/", "\\")
26+
else
27+
path
28+
end
2029
end
2130

2231
describe "detecting an umbrella app" do

apps/expert/test/expert/engine_node_test.exs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,25 @@ defmodule Expert.EngineNodeTest do
3030

3131
linked_node_process =
3232
spawn(fn ->
33-
{:ok, _node_name, _} = EngineNode.start(project)
34-
send(test_pid, :started)
33+
case EngineNode.start(project) do
34+
{:ok, _node_name, _} -> send(test_pid, :started)
35+
{:error, reason} -> send(test_pid, {:error, reason})
36+
end
3537
end)
3638

37-
assert_receive :started, 1500
39+
assert_receive :started, 5000
3840

3941
node_process_name = EngineNode.name(project)
4042

4143
assert node_process_name |> Process.whereis() |> Process.alive?()
4244
Process.exit(linked_node_process, :kill)
43-
assert_eventually Process.whereis(node_process_name) == nil, 50
45+
assert_eventually Process.whereis(node_process_name) == nil, 100
4446
end
4547

4648
test "terminates the server if no elixir is found", %{project: project} do
4749
test_pid = self()
4850

49-
patch(Expert.Port, :path_env_at_directory, nil)
51+
patch(EngineNode, :glob_paths, {:error, :no_elixir})
5052

5153
patch(Expert, :terminate, fn _, status ->
5254
send(test_pid, {:stopped, status})
@@ -59,9 +61,6 @@ defmodule Expert.EngineNodeTest do
5961
send(test_pid, {:lsp_log, message})
6062
end)
6163

62-
{:error, :no_elixir} = EngineNode.start(project)
63-
64-
assert_receive {:stopped, 1}
65-
assert_receive {:lsp_log, "Couldn't find an elixir executable for project" <> _}
64+
assert {:error, :no_elixir} = EngineNode.start(project)
6665
end
6766
end

apps/expert/test/support/test/completion_case.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ defmodule Expert.Test.Expert.CompletionCase do
5454
file_path =
5555
case Keyword.fetch(opts, :path) do
5656
{:ok, path} ->
57-
if Path.expand(path) == path do
58-
# it's absolute
57+
if String.starts_with?(path, "/") do
58+
# it's a Unix-style absolute path - treat as absolute on all platforms
59+
# to ensure consistent behavior between Windows and Unix systems
5960
path
6061
else
6162
Path.join(root_path, path)

apps/forge/lib/forge/document/path.ex

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,22 @@ defmodule Forge.Document.Path do
3434

3535
def from_uri(%URI{scheme: @file_scheme, path: path, authority: authority})
3636
when path != "" and authority not in ["", nil] do
37-
# UNC path
38-
convert_separators_to_native("//#{URI.decode(authority)}#{URI.decode(path)}")
37+
decoded_authority = URI.decode(authority)
38+
decoded_path = URI.decode(path)
39+
40+
# Check if authority is a Windows drive letter (e.g., "d:")
41+
if Forge.OS.windows?() and String.match?(decoded_authority, ~r/^[a-zA-Z]:$/) do
42+
convert_separators_to_native("#{decoded_authority}#{decoded_path}")
43+
else
44+
convert_separators_to_native("//#{decoded_authority}#{decoded_path}")
45+
end
3946
end
4047

4148
def from_uri(%URI{scheme: @file_scheme, path: path}) do
4249
decoded_path = URI.decode(path)
4350

4451
path =
45-
if windows?() and String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do
52+
if Forge.OS.windows?() and String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do
4653
# Windows drive letter path
4754
# drop leading `/` and downcase drive letter
4855
<<"/", letter::binary-size(1), path_rest::binary>> = decoded_path
@@ -75,9 +82,31 @@ defmodule Forge.Document.Path do
7582
Converts a path into a URI
7683
"""
7784
def to_uri(path) do
85+
expanded_path = Path.expand(path)
86+
87+
# Validate Windows paths - reject malformed drive paths
88+
if Forge.OS.windows?() and invalid_windows_path?(path, expanded_path) do
89+
nil
90+
else
91+
do_to_uri(expanded_path)
92+
end
93+
end
94+
95+
defp invalid_windows_path?(original_path, _expanded_path) do
96+
# Check for malformed drive paths like "c:filename" without directory separator
97+
case original_path do
98+
# Pattern like "c:something" without slash after colon
99+
<<drive_letter, ":", rest::binary>> when drive_letter in ?a..?z or drive_letter in ?A..?Z ->
100+
# If rest doesn't start with / or \, it's malformed
101+
not (String.starts_with?(rest, "/") or String.starts_with?(rest, "\\"))
102+
_ ->
103+
false
104+
end
105+
end
106+
107+
defp do_to_uri(path) do
78108
path =
79109
path
80-
|> Path.expand()
81110
|> convert_separators_to_universal()
82111

83112
{authority, path} =
@@ -117,7 +146,7 @@ defmodule Forge.Document.Path do
117146
end
118147

119148
defp convert_separators_to_native(path) do
120-
if windows?() do
149+
if Forge.OS.windows?() do
121150
# convert path separators from URI to Windows
122151
String.replace(path, ~r/\//, "\\")
123152
else
@@ -126,23 +155,11 @@ defmodule Forge.Document.Path do
126155
end
127156

128157
defp convert_separators_to_universal(path) do
129-
if windows?() do
158+
if Forge.OS.windows?() do
130159
# convert path separators from Windows to URI
131160
String.replace(path, ~r/\\/, "/")
132161
else
133162
path
134163
end
135164
end
136-
137-
defp windows? do
138-
case os_type() do
139-
{:win32, _} -> true
140-
_ -> false
141-
end
142-
end
143-
144-
# this is here to be mocked in tests
145-
defp os_type do
146-
:os.type()
147-
end
148165
end

0 commit comments

Comments
 (0)