1Code.require_file("../../test_helper.exs", __DIR__)
2
3defmodule Mix.Tasks.CompileTest do
4  use MixTest.Case
5
6  if System.otp_release() >= "24" do
7    defmacro position(line, column), do: {line, column}
8  else
9    defmacro position(line, _column), do: line
10  end
11
12  defmodule CustomCompilers do
13    def project do
14      [compilers: [:elixir, :app, :custom]]
15    end
16  end
17
18  defmodule DepsApp do
19    def project do
20      [app: :deps_app, version: "0.1.0", deps: [{:ok, "0.1.0", path: "deps/ok"}]]
21    end
22  end
23
24  defmodule WrongPath do
25    def project do
26      [app: :apps_path_bug, apps_path: "this_path_does_not_exist"]
27    end
28  end
29
30  setup do
31    Mix.Project.push(MixTest.Case.Sample)
32    :ok
33  end
34
35  test "compiles --list with mixfile" do
36    Mix.Task.run("compile", ["--list"])
37
38    msg = "\nEnabled compilers: yecc, leex, erlang, elixir, app, protocols"
39    assert_received {:mix_shell, :info, [^msg]}
40
41    assert_received {:mix_shell, :info, ["mix compile.elixir    # " <> _]}
42  end
43
44  test "compiles --list with custom mixfile" do
45    Mix.Project.pop()
46    Mix.Project.push(CustomCompilers)
47    Mix.Task.run("compile", ["--list"])
48    assert_received {:mix_shell, :info, ["\nEnabled compilers: elixir, app, custom, protocols"]}
49  end
50
51  test "compiles does not require all compilers available on manifest" do
52    Mix.Project.pop()
53    Mix.Project.push(CustomCompilers)
54    assert Mix.Tasks.Compile.manifests() |> Enum.map(&Path.basename/1) == ["compile.elixir"]
55  end
56
57  test "compiles a project with cached deps information" do
58    Mix.Project.pop()
59
60    in_fixture("deps_status", fn ->
61      Mix.Project.push(DepsApp)
62
63      File.mkdir_p!("lib")
64
65      File.write!("lib/a.ex", """
66      root = File.cwd!()
67      File.cd!("lib", fn ->
68        %{ok: path} = Mix.Project.deps_paths()
69
70        if Path.relative_to(path, root) != "deps/ok" do
71          raise "non cached path"
72        end
73      end)
74      """)
75
76      assert Mix.Task.run("compile", ["--force", "--from-mix-deps-compile"]) == {:ok, []}
77    end)
78  end
79
80  test "compiles a project with mixfile" do
81    in_fixture("no_mixfile", fn ->
82      assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
83      assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam")
84      assert File.regular?("_build/dev/lib/sample/ebin/sample.app")
85      assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
86      assert_received {:mix_shell, :info, ["Generated sample app"]}
87      assert File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam")
88
89      # Noop
90      Mix.Task.clear()
91      assert Mix.Task.run("compile", ["--verbose"]) == {:noop, []}
92      refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
93
94      # Consolidates protocols if manifest is out of date
95      File.rm("_build/dev/lib/sample/.mix/compile.protocols")
96      Mix.Task.clear()
97      assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
98      refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
99      assert File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam")
100
101      # Purge so consolidated is picked up
102      purge([Enumerable])
103      assert Mix.Tasks.App.Start.run(["--verbose"]) == :ok
104      assert Protocol.consolidated?(Enumerable)
105    end)
106  end
107
108  # A.info/0 is loaded dynamically
109  @compile {:no_warn_undefined, {A, :info, 0}}
110
111  test "adds Logger application metadata" do
112    import ExUnit.CaptureLog
113
114    in_fixture("no_mixfile", fn ->
115      Process.put({MixTest.Case.Sample, :application}, extra_applications: [:logger])
116
117      File.write!("lib/a.ex", """
118      defmodule A do
119      require Logger
120      def info, do: Logger.info("hello")
121      end
122      """)
123
124      assert Mix.Task.run("compile", []) == {:ok, []}
125
126      try do
127        assert capture_log([metadata: [:application]], &A.info/0) =~ "application=sample"
128      after
129        purge([A])
130      end
131    end)
132  end
133
134  test "returns errors from compilers when --return-errors is set" do
135    in_fixture("no_mixfile", fn ->
136      File.write!("lib/a.ex", """
137      defmodule A do
138        def my_fn(), do: $$$
139      end
140      """)
141
142      file = Path.absname("lib/a.ex")
143
144      ExUnit.CaptureIO.capture_io(fn ->
145        assert {:error, [diagnostic]} = Mix.Task.run("compile", ["--return-errors"])
146
147        assert %Mix.Task.Compiler.Diagnostic{
148                 file: ^file,
149                 severity: :error,
150                 position: 2,
151                 message: "** (SyntaxError) lib/a.ex:2:" <> _,
152                 compiler_name: "Elixir"
153               } = diagnostic
154      end)
155    end)
156  end
157
158  test "calling raise inside a macro returns a diagnostic with a position" do
159    in_fixture("no_mixfile", fn ->
160      File.write!("lib/a.ex", """
161      defmodule A do
162        defmacro custom_macro do
163          raise "error"
164        end
165      end
166      """)
167
168      File.write!("lib/b.ex", """
169      defmodule B do
170        require A
171        A.custom_macro()
172      end
173      """)
174
175      file = Path.absname("lib/b.ex")
176
177      ExUnit.CaptureIO.capture_io(fn ->
178        assert {:error, [diagnostic]} = Mix.Task.run("compile", ["--return-errors"])
179
180        assert %Mix.Task.Compiler.Diagnostic{
181                 file: ^file,
182                 severity: :error,
183                 position: 3,
184                 message: "** (RuntimeError) error\n    expanding macro: A.custom_macro/0" <> _,
185                 compiler_name: "Elixir"
186               } = diagnostic
187      end)
188    end)
189  end
190
191  test "returns syntax error from an Erlang file when --return-errors is set" do
192    in_fixture("no_mixfile", fn ->
193      import ExUnit.CaptureIO
194
195      file = Path.absname("src/a.erl")
196      File.mkdir!("src")
197
198      File.write!(file, """
199      -module(b).
200      def b(), do: b
201      """)
202
203      assert File.regular?(file)
204
205      capture_io(fn ->
206        assert {:error, [diagnostic]} = Mix.Task.run("compile", ["--force", "--return-errors"])
207
208        assert %Mix.Task.Compiler.Diagnostic{
209                 compiler_name: "erl_parse",
210                 file: ^file,
211                 message: "syntax error before: b",
212                 position: position(2, 5),
213                 severity: :error
214               } = diagnostic
215      end)
216
217      refute File.regular?("ebin/Elixir.A.beam")
218      refute File.regular?("ebin/Elixir.B.beam")
219    end)
220  end
221
222  test "skip protocol consolidation when --no-protocol-consolidation" do
223    in_fixture("no_mixfile", fn ->
224      File.rm("_build/dev/lib/sample/.mix/compile.protocols")
225      assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:ok, []}
226      assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam")
227      refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam")
228    end)
229  end
230
231  test "loads consolidated protocols even on --no-compile" do
232    in_fixture("no_mixfile", fn ->
233      File.rm("_build/dev/lib/sample/.mix/compile.protocols")
234      consolidated = "_build/dev/lib/sample/consolidated" |> Path.expand() |> to_charlist()
235
236      assert Mix.Task.run("compile") == {:ok, []}
237      assert File.regular?("_build/dev/lib/sample/consolidated/Elixir.Enumerable.beam")
238      assert consolidated in :code.get_path()
239
240      Code.delete_path("_build/dev/lib/sample/consolidated")
241      assert Mix.Task.rerun("compile", ["--no-compile"]) == {:noop, []}
242      assert consolidated in :code.get_path()
243    end)
244  end
245
246  test "loads Mix config with --erl-config" do
247    in_fixture("no_mixfile", fn ->
248      File.write!("mix.config", "{erl_config_app, [{value, true}]}.")
249      assert Mix.Task.run("compile", ["--erl-config", "mix.config"]) == {:ok, []}
250      assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam")
251      assert Application.get_env(:erl_config_app, :value)
252    end)
253  after
254    Application.delete_env(:erl_config_app, :value)
255  end
256
257  test "runs after_compiler callback once" do
258    in_fixture("no_mixfile", fn ->
259      callback = fn result -> send(self(), result) end
260
261      assert Mix.Task.Compiler.after_compiler(:elixir, callback) == :ok
262      assert Mix.Task.rerun("compile", []) == {:ok, []}
263      assert_received {:ok, []}
264
265      Mix.Task.clear()
266      assert Mix.Task.rerun("compile", []) == {:noop, []}
267      refute_received {:noop, []}
268
269      Mix.Task.clear()
270      assert Mix.Task.Compiler.after_compiler(:elixir, callback) == :ok
271      assert Mix.Task.run("compile", []) == {:noop, []}
272      assert_received {:noop, []}
273    end)
274  end
275
276  test "does not crash on a project with bad path" do
277    Mix.Project.pop()
278    Mix.Project.push(WrongPath)
279
280    ExUnit.CaptureIO.capture_io(fn ->
281      assert Mix.Task.run("compile", ["--no-protocol-consolidation"]) == {:noop, []}
282    end)
283  end
284
285  test "validates compile_env" do
286    in_fixture("no_mixfile", fn ->
287      File.mkdir_p!("config")
288
289      File.write!("config/config.exs", """
290      import Config
291      config :sample, :hello, System.get_env("MIX_SAMPLE_HELLO")
292      """)
293
294      File.write!("lib/a.ex", """
295      defmodule A do
296        require Application
297        _ = Application.compile_env(:sample, :hello)
298      end
299      """)
300
301      System.put_env("MIX_SAMPLE_HELLO", "compile")
302      Mix.Tasks.Loadconfig.run([])
303      assert Mix.Tasks.Compile.All.run([]) == {:ok, []}
304
305      System.put_env("MIX_SAMPLE_HELLO", "runtime")
306      Mix.Tasks.Loadconfig.run([])
307      Application.unload(:sample)
308
309      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
310               assert_raise ErlangError, fn -> Mix.Tasks.Compile.All.run([]) end
311             end) =~
312               " the application :sample has a different value set for key :hello during runtime compared to compile time"
313
314      # Can start if compile env matches
315      System.put_env("MIX_SAMPLE_HELLO", "compile")
316      Mix.Tasks.Loadconfig.run([])
317      assert Mix.Tasks.App.Start.run([]) == :ok
318    end)
319  after
320    System.delete_env("MIX_SAMPLE_HELLO")
321    Application.delete_env(:sample, :hello, persistent: true)
322  end
323end
324