1Code.require_file("../test_helper.exs", __DIR__)
2
3defmodule Mix.DepTest do
4  use MixTest.Case
5
6  defmodule DepsApp do
7    def project do
8      [
9        app: :deps_app,
10        deps: [
11          {:ok, "0.1.0", path: "deps/ok"},
12          {:invalidvsn, "0.2.0", path: "deps/invalidvsn"},
13          {:invalidapp, "0.1.0", path: "deps/invalidapp"},
14          {:noappfile, "0.1.0", path: "deps/noappfile"},
15          {:uncloned, git: "https://github.com/elixir-lang/uncloned.git"},
16          {:optional, git: "https://github.com/elixir-lang/optional.git", optional: true}
17        ]
18      ]
19    end
20  end
21
22  defmodule ProcessDepsApp do
23    def project do
24      [app: :process_deps_app, deps: Process.get(:mix_deps)]
25    end
26  end
27
28  defp with_deps(deps, fun) do
29    Process.put(:mix_deps, deps)
30    Mix.Project.push(ProcessDepsApp)
31    fun.()
32  after
33    Mix.Project.pop()
34  end
35
36  defp assert_wrong_dependency(deps) do
37    with_deps(deps, fn ->
38      assert_raise Mix.Error, ~r"Dependency specified in the wrong format", fn ->
39        Mix.Dep.load_on_environment([])
40      end
41    end)
42  end
43
44  test "clear deps cache" do
45    Mix.Project.push(DepsApp)
46
47    Mix.Dep.cached()
48    key = {:cached_deps, DepsApp}
49
50    {env_target, deps} = Mix.State.read_cache(key)
51    assert env_target == {Mix.env(), Mix.target()}
52    assert length(deps) == 6
53
54    Mix.Dep.clear_cached()
55    refute Mix.State.read_cache(key)
56  end
57
58  test "extracts all dependencies from the given project" do
59    in_fixture("deps_status", fn ->
60      Mix.Project.push(DepsApp)
61
62      deps = Mix.Dep.load_on_environment([])
63      assert length(deps) == 6
64      assert Enum.find(deps, &match?(%Mix.Dep{app: :ok, status: {:ok, _}}, &1))
65      assert Enum.find(deps, &match?(%Mix.Dep{app: :invalidvsn, status: {:invalidvsn, :ok}}, &1))
66      assert Enum.find(deps, &match?(%Mix.Dep{app: :invalidapp, status: {:invalidapp, _}}, &1))
67      assert Enum.find(deps, &match?(%Mix.Dep{app: :noappfile, status: {:noappfile, {_, _}}}, &1))
68      assert Enum.find(deps, &match?(%Mix.Dep{app: :uncloned, status: {:unavailable, _}}, &1))
69      assert Enum.find(deps, &match?(%Mix.Dep{app: :optional, status: {:unavailable, _}}, &1))
70    end)
71  end
72
73  test "extracts all dependencies paths/scms from the given project" do
74    in_fixture("deps_status", fn ->
75      Mix.Project.push(DepsApp)
76
77      apps = Mix.Project.deps_apps()
78      assert length(apps) == 6
79      assert :ok in apps
80      assert :uncloned in apps
81
82      paths = Mix.Project.deps_paths()
83      assert map_size(paths) == 6
84      assert paths[:ok] =~ "deps/ok"
85      assert paths[:uncloned] =~ "deps/uncloned"
86
87      paths = Mix.Project.deps_scms()
88      assert map_size(paths) == 6
89      assert paths[:ok] == Mix.SCM.Path
90      assert paths[:uncloned] == Mix.SCM.Git
91    end)
92  end
93
94  test "fails on invalid dependencies" do
95    assert_wrong_dependency([{:ok}])
96    assert_wrong_dependency([{:ok, nil}])
97    assert_wrong_dependency([{:ok, nil, []}])
98  end
99
100  test "use requirements for dependencies" do
101    deps = [{:ok, "~> 0.1", path: "deps/ok"}]
102
103    with_deps(deps, fn ->
104      in_fixture("deps_status", fn ->
105        deps = Mix.Dep.load_on_environment([])
106        assert Enum.find(deps, &match?(%Mix.Dep{app: :ok, status: {:ok, _}}, &1))
107      end)
108    end)
109  end
110
111  test "raises when no SCM is specified" do
112    deps = [{:ok, "~> 0.1", not_really: :ok}]
113
114    with_deps(deps, fn ->
115      in_fixture("deps_status", fn ->
116        send(self(), {:mix_shell_input, :yes?, false})
117        msg = "Could not find an SCM for dependency :ok from Mix.DepTest.ProcessDepsApp"
118        assert_raise Mix.Error, msg, fn -> Mix.Dep.load_on_environment([]) end
119      end)
120    end)
121  end
122
123  test "does not set the manager before the dependency was loaded" do
124    # It is important to not eagerly set the manager because the dependency
125    # needs to be loaded (i.e. available in the file system) in order to get
126    # the proper manager.
127    Mix.Project.push(DepsApp)
128
129    {_, true, _} =
130      Mix.Dep.Converger.converge(false, [], nil, fn dep, acc, lock ->
131        assert is_nil(dep.manager)
132        {dep, acc or true, lock}
133      end)
134  end
135
136  test "raises on invalid deps req" do
137    deps = [{:ok, "+- 0.1.0", path: "deps/ok"}]
138
139    with_deps(deps, fn ->
140      in_fixture("deps_status", fn ->
141        assert_raise Mix.Error, ~r"Invalid requirement", fn ->
142          Mix.Dep.load_on_environment([])
143        end
144      end)
145    end)
146  end
147
148  test "nested deps come first" do
149    deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]
150
151    with_deps(deps, fn ->
152      in_fixture("deps_status", fn ->
153        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
154      end)
155    end)
156  end
157
158  test "nested optional deps are never added" do
159    deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]
160
161    with_deps(deps, fn ->
162      in_fixture("deps_status", fn ->
163        File.write!("custom/deps_repo/mix.exs", """
164        defmodule DepsRepo do
165          use Mix.Project
166
167          def project do
168            [app: :deps_repo,
169             version: "0.1.0",
170             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
171          end
172        end
173        """)
174
175        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:deps_repo]
176      end)
177    end)
178  end
179
180  test "nested deps with convergence" do
181    deps = [
182      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
183      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
184    ]
185
186    with_deps(deps, fn ->
187      in_fixture("deps_status", fn ->
188        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
189      end)
190    end)
191  end
192
193  test "nested deps with convergence and managers" do
194    Process.put(:custom_deps_git_repo_opts, manager: :make)
195
196    deps = [
197      {:deps_repo, "0.1.0", path: "custom/deps_repo", manager: :rebar},
198      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
199    ]
200
201    with_deps(deps, fn ->
202      in_fixture("deps_status", fn ->
203        [dep1, dep2] = Mix.Dep.load_on_environment([])
204        assert dep1.manager == nil
205        assert dep2.manager == :rebar
206      end)
207    end)
208  end
209
210  test "nested deps with optional matching" do
211    Process.put(:custom_deps_git_repo_opts, optional: true)
212
213    # deps_repo brings git_repo but it is optional
214    deps = [
215      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
216      {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
217    ]
218
219    with_deps(deps, fn ->
220      in_fixture("deps_status", fn ->
221        File.mkdir_p!("custom/deps_repo/lib")
222
223        File.write!("custom/deps_repo/lib/a.ex", """
224        # Check that the child dependency is top_level and optional
225        [%Mix.Dep{app: :git_repo, top_level: true, opts: opts}] = Mix.Dep.cached()
226        true = Keyword.fetch!(opts, :optional)
227        """)
228
229        Mix.Tasks.Deps.Get.run([])
230        Mix.Tasks.Deps.Compile.run([])
231      end)
232    end)
233  end
234
235  test "nested deps with convergence and optional dependencies" do
236    deps = [
237      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
238      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
239    ]
240
241    with_deps(deps, fn ->
242      in_fixture("deps_status", fn ->
243        File.write!("custom/deps_repo/mix.exs", """
244        defmodule DepsRepo do
245          use Mix.Project
246
247          def project do
248            [app: :deps_repo,
249             version: "0.1.0",
250             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
251          end
252        end
253        """)
254
255        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) == [:git_repo, :deps_repo]
256      end)
257    end)
258  end
259
260  test "nested deps with optional dependencies and cousin conflict" do
261    deps = [
262      {:deps_repo1, "0.1.0", path: "custom/deps_repo1"},
263      {:deps_repo2, "0.1.0", path: "custom/deps_repo2"}
264    ]
265
266    with_deps(deps, fn ->
267      in_fixture("deps_status", fn ->
268        File.mkdir_p!("custom/deps_repo1")
269
270        File.write!("custom/deps_repo1/mix.exs", """
271        defmodule DepsRepo1 do
272          use Mix.Project
273
274          def project do
275            [app: :deps_repo1,
276             version: "0.1.0",
277             deps: [{:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), optional: true}]]
278          end
279        end
280        """)
281
282        File.mkdir_p!("custom/deps_repo2")
283
284        File.write!("custom/deps_repo2/mix.exs", """
285        defmodule DepsRepo2 do
286          use Mix.Project
287
288          def project do
289            [app: :deps_repo2,
290             version: "0.1.0",
291             deps: [{:git_repo, "0.2.0", path: "somewhere"}]]
292          end
293        end
294        """)
295
296        Mix.Tasks.Deps.run([])
297        assert_received {:mix_shell, :info, ["* deps_repo1" <> _]}
298        assert_received {:mix_shell, :info, [_]}
299        assert_received {:mix_shell, :info, ["* deps_repo2" <> _]}
300        assert_received {:mix_shell, :info, [_]}
301        assert_received {:mix_shell, :info, ["* git_repo" <> _]}
302        assert_received {:mix_shell, :info, [msg]}
303        assert msg =~ "different specs were given for the git_repo"
304      end)
305    end)
306  end
307
308  test "deps with system_env set" do
309    file_path = tmp_path("load dependency with env vars/dep-test")
310    dep_path = tmp_path("rebar_dep")
311
312    system_env = [{"FILE_FROM_ENV", file_path}, {"CONTENTS_FROM_ENV", "contents dep test"}]
313    deps = [{:rebar_dep, path: dep_path, app: false, manager: :rebar, system_env: system_env}]
314
315    with_deps(deps, fn ->
316      in_tmp("load dependency with env vars", fn ->
317        Mix.Dep.load_on_environment([])
318        assert {:ok, "contents dep test"} = File.read(file_path)
319      end)
320    end)
321  end
322
323  test "diverged with system_env set" do
324    Process.put(:custom_deps_git_repo_opts, system_env: [{"FOO", "BAR"}])
325
326    deps = [
327      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
328      {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
329    ]
330
331    with_deps(deps, fn ->
332      in_fixture("deps_status", fn ->
333        [git_repo, _] = Mix.Dep.load_on_environment([])
334        %{app: :git_repo, status: {:overridden, _}} = git_repo
335      end)
336    end)
337  end
338
339  ## Remote converger
340
341  defmodule IdentityRemoteConverger do
342    @behaviour Mix.RemoteConverger
343
344    def remote?(%Mix.Dep{app: :deps_repo}), do: false
345    def remote?(%Mix.Dep{}), do: true
346    def deps(_dep, _lock), do: []
347    def post_converge, do: :ok
348
349    def converge(deps, lock) do
350      Process.put(:remote_converger, deps)
351      lock
352    end
353  end
354
355  test "remote converger" do
356    deps = [
357      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
358      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo")}
359    ]
360
361    with_deps(deps, fn ->
362      Mix.RemoteConverger.register(IdentityRemoteConverger)
363
364      in_fixture("deps_status", fn ->
365        Mix.Tasks.Deps.Get.run([])
366
367        message = "* Getting git_repo (#{fixture_path("git_repo")})"
368        assert_received {:mix_shell, :info, [^message]}
369
370        assert Process.get(:remote_converger)
371      end)
372    end)
373  after
374    Mix.RemoteConverger.register(nil)
375  end
376
377  defmodule DivergingRemoteConverger do
378    @behaviour Mix.RemoteConverger
379
380    def remote?(%Mix.Dep{app: :deps_repo, scm: Mix.SCM.Path}), do: true
381    def remote?(%Mix.Dep{app: :git_repo, scm: Mix.SCM.Path}), do: true
382    def remote?(%Mix.Dep{}), do: false
383    def deps(%Mix.Dep{app: :deps_repo}, _lock), do: [{:git_repo, path: "custom/git_repo"}]
384    def deps(%Mix.Dep{app: :git_repo}, _lock), do: []
385    def post_converge, do: :ok
386
387    def converge(_deps, lock) do
388      lock
389      |> Map.put(:deps_repo, :custom)
390      |> Map.put(:git_repo, :custom)
391    end
392  end
393
394  test "converger detects diverged deps from remote converger" do
395    deps = [
396      {:deps_on_git_repo, "0.2.0", git: MixTest.Case.fixture_path("deps_on_git_repo")},
397      {:deps_repo, "0.1.0", path: "custom/deps_repo"}
398    ]
399
400    with_deps(deps, fn ->
401      Mix.RemoteConverger.register(DivergingRemoteConverger)
402
403      in_fixture("deps_status", fn ->
404        assert_raise Mix.Error, fn ->
405          Mix.Tasks.Deps.Get.run([])
406        end
407
408        assert_received {:mix_shell, :error, ["Dependencies have diverged:"]}
409      end)
410    end)
411  after
412    Mix.RemoteConverger.register(nil)
413  end
414
415  test "pass dependencies to remote converger in defined order" do
416    deps = [
417      {:ok, "0.1.0", path: "deps/ok"},
418      {:invalidvsn, "0.2.0", path: "deps/invalidvsn"},
419      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
420      {:invalidapp, "0.1.0", path: "deps/invalidapp"},
421      {:noappfile, "0.1.0", path: "deps/noappfile"}
422    ]
423
424    with_deps(deps, fn ->
425      Mix.RemoteConverger.register(IdentityRemoteConverger)
426
427      in_fixture("deps_status", fn ->
428        Mix.Tasks.Deps.Get.run([])
429
430        deps = Process.get(:remote_converger) |> Enum.map(& &1.app)
431        assert deps == [:ok, :invalidvsn, :deps_repo, :invalidapp, :noappfile, :git_repo]
432      end)
433    end)
434  after
435    Mix.RemoteConverger.register(nil)
436  end
437
438  defmodule RaiseRemoteConverger do
439    @behaviour Mix.RemoteConverger
440
441    def remote?(_app), do: false
442    def deps(_dep, _lock), do: :ok
443    def post_converge, do: :ok
444
445    def converge(_deps, lock) do
446      Process.put(:remote_converger, true)
447      lock
448    end
449  end
450
451  test "remote converger is not invoked if deps diverge" do
452    deps = [
453      {:deps_repo, "0.1.0", path: "custom/deps_repo"},
454      {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
455    ]
456
457    with_deps(deps, fn ->
458      Mix.RemoteConverger.register(RaiseRemoteConverger)
459
460      in_fixture("deps_status", fn ->
461        assert_raise Mix.Error, fn ->
462          Mix.Tasks.Deps.Get.run([])
463        end
464
465        assert_received {:mix_shell, :error, ["Dependencies have diverged:"]}
466        refute Process.get(:remote_converger)
467      end)
468    end)
469  after
470    Mix.RemoteConverger.register(nil)
471  end
472
473  test "remote converger is not invoked if deps graph has cycles" do
474    deps = [{:app1, "0.1.0", path: "app1"}, {:app2, "0.1.0", path: "app2"}]
475
476    with_deps(deps, fn ->
477      Mix.RemoteConverger.register(RaiseRemoteConverger)
478
479      in_fixture("deps_cycle", fn ->
480        assert_raise Mix.Error, ~r/cycles in the dependency graph/, fn ->
481          Mix.Tasks.Deps.Get.run([])
482        end
483
484        refute Process.get(:remote_converger)
485      end)
486    end)
487  after
488    Mix.RemoteConverger.register(nil)
489  end
490
491  test "deps_paths" do
492    deps = [
493      {:abc_repo, "0.1.0", path: "custom/abc_repo"},
494      {:deps_repo, "0.1.0", path: "custom/deps_repo"}
495    ]
496
497    with_deps(deps, fn ->
498      in_fixture("deps_status", fn ->
499        assert Enum.map(Mix.Dep.load_on_environment([]), & &1.app) ==
500                 [:git_repo, :abc_repo, :deps_repo]
501
502        assert Map.keys(Mix.Project.deps_paths()) == [:abc_repo, :deps_repo, :git_repo]
503
504        assert Map.keys(Mix.Project.deps_paths(depth: 1)) == [:abc_repo, :deps_repo]
505        assert Map.keys(Mix.Project.deps_paths(depth: 2)) == [:abc_repo, :deps_repo, :git_repo]
506        assert Map.keys(Mix.Project.deps_paths(depth: 3)) == [:abc_repo, :deps_repo, :git_repo]
507
508        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo])) == [:abc_repo]
509        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo])) == [:deps_repo, :git_repo]
510        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo])) == [:git_repo]
511
512        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo], depth: 1)) == [:abc_repo]
513        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo], depth: 1)) == [:deps_repo]
514        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo], depth: 1)) == [:git_repo]
515
516        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo], depth: 2)) == [:abc_repo]
517        assert Map.keys(Mix.Project.deps_paths(parents: [:git_repo], depth: 2)) == [:git_repo]
518
519        assert Map.keys(Mix.Project.deps_paths(parents: [:deps_repo], depth: 2)) ==
520                 [:deps_repo, :git_repo]
521
522        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo])) ==
523                 [:abc_repo, :deps_repo, :git_repo]
524
525        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo], depth: 1)) ==
526                 [:abc_repo, :deps_repo]
527
528        assert Map.keys(Mix.Project.deps_paths(parents: [:abc_repo, :deps_repo], depth: 2)) ==
529                 [:abc_repo, :deps_repo, :git_repo]
530      end)
531    end)
532  end
533
534  describe "only handling" do
535    test "extracts deps matching environment" do
536      deps = [
537        {:foo, github: "elixir-lang/foo"},
538        {:bar, github: "elixir-lang/bar", only: :other_env}
539      ]
540
541      with_deps(deps, fn ->
542        in_fixture("deps_status", fn ->
543          deps = Mix.Dep.load_on_environment(env: :other_env)
544          assert length(deps) == 2
545
546          deps = Mix.Dep.load_on_environment([])
547          assert length(deps) == 2
548
549          assert [dep] = Mix.Dep.load_on_environment(env: :prod)
550          assert dep.app == :foo
551        end)
552      end)
553    end
554
555    test "fetches parent deps matching specified env" do
556      deps = [{:only, github: "elixir-lang/only", only: [:dev]}]
557
558      with_deps(deps, fn ->
559        in_fixture("deps_status", fn ->
560          Mix.Tasks.Deps.Get.run(["--only", "prod"])
561          refute_received {:mix_shell, :info, ["* Getting" <> _]}
562
563          assert_raise Mix.Error, "Can't continue due to errors on dependencies", fn ->
564            Mix.Tasks.Deps.Loadpaths.run([])
565          end
566
567          Mix.State.clear_cache()
568          Mix.env(:prod)
569          Mix.Tasks.Deps.Loadpaths.run([])
570        end)
571      end)
572    end
573
574    test "selects only prod dependencies on nested deps" do
575      Process.put(:custom_deps_git_repo_opts, only: :test)
576      deps = [{:deps_repo, "0.1.0", path: "custom/deps_repo"}]
577
578      with_deps(deps, fn ->
579        in_fixture("deps_status", fn ->
580          loaded = Mix.Dep.load_on_environment([])
581          assert [:deps_repo] = Enum.map(loaded, & &1.app)
582
583          loaded = Mix.Dep.load_on_environment(env: :test)
584          assert [:deps_repo] = Enum.map(loaded, & &1.app)
585        end)
586      end)
587    end
588
589    test "conflicts on nested deps" do
590      # deps_repo wants all git_repo, git_repo is restricted to only test
591      deps = [
592        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
593        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
594      ]
595
596      with_deps(deps, fn ->
597        in_fixture("deps_status", fn ->
598          loaded = Mix.Dep.load_on_environment([])
599          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
600          assert [divergedonly: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
601
602          loaded = Mix.Dep.load_on_environment(env: :dev)
603          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
604          assert [divergedonly: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
605
606          loaded = Mix.Dep.load_on_environment(env: :test)
607          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
608          assert [divergedonly: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
609
610          Mix.Tasks.Deps.run([])
611          assert_received {:mix_shell, :info, ["* deps_repo" <> _]}
612          assert_received {:mix_shell, :info, [_]}
613          assert_received {:mix_shell, :info, ["* git_repo" <> _]}
614          assert_received {:mix_shell, :info, [msg]}
615          assert msg =~ "Remove the :only restriction from your dep"
616        end)
617      end)
618    end
619
620    test "does not conflict with optional deps on nested deps" do
621      Process.put(:custom_deps_git_repo_opts, optional: true)
622
623      # deps_repo wants all git_repo, git_repo is restricted to only test
624      deps = [
625        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
626        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
627      ]
628
629      with_deps(deps, fn ->
630        in_fixture("deps_status", fn ->
631          loaded = Mix.Dep.load_on_environment([])
632          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
633          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
634
635          loaded = Mix.Dep.load_on_environment(env: :dev)
636          assert [:deps_repo] = Enum.map(loaded, & &1.app)
637          assert [noappfile: {_, _}] = Enum.map(loaded, & &1.status)
638
639          loaded = Mix.Dep.load_on_environment(env: :test)
640          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
641          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
642        end)
643      end)
644    end
645
646    test "does not conflict on valid subsets on nested deps" do
647      # deps_repo wants git_repo for prod, git_repo is restricted to only prod and test
648      deps = [
649        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
650        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: [:prod, :test]}
651      ]
652
653      with_deps(deps, fn ->
654        in_fixture("deps_status", fn ->
655          loaded = Mix.Dep.load_on_environment([])
656          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
657          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
658
659          loaded = Mix.Dep.load_on_environment(env: :dev)
660          assert [] = Enum.map(loaded, & &1.app)
661
662          loaded = Mix.Dep.load_on_environment(env: :test)
663          assert [:git_repo] = Enum.map(loaded, & &1.app)
664          assert [unavailable: _] = Enum.map(loaded, & &1.status)
665
666          loaded = Mix.Dep.load_on_environment(env: :prod)
667          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
668          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
669        end)
670      end)
671    end
672
673    test "conflicts on invalid only subset on nested deps" do
674      # deps_repo wants git_repo for dev, git_repo is restricted to only test
675      deps = [
676        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :dev},
677        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), only: [:test]}
678      ]
679
680      with_deps(deps, fn ->
681        in_fixture("deps_status", fn ->
682          loaded = Mix.Dep.load_on_environment([])
683          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
684          assert [divergedonly: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
685
686          loaded = Mix.Dep.load_on_environment(env: :dev)
687          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
688          assert [divergedonly: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
689
690          loaded = Mix.Dep.load_on_environment(env: :test)
691          assert [:git_repo] = Enum.map(loaded, & &1.app)
692          assert [unavailable: _] = Enum.map(loaded, & &1.status)
693
694          Mix.Tasks.Deps.run([])
695          assert_received {:mix_shell, :info, ["* deps_repo" <> _]}
696          assert_received {:mix_shell, :info, [_]}
697          assert_received {:mix_shell, :info, ["* git_repo" <> _]}
698          assert_received {:mix_shell, :info, [msg]}
699          assert msg =~ "Ensure you specify at least the same environments in :only in your dep"
700        end)
701      end)
702    end
703
704    test "does not conflict with valid only in both parent and child on nested deps" do
705      Process.put(:custom_deps_git_repo_opts, only: :test)
706
707      # deps_repo has environment set to test so it loads the deps_git_repo set to test too
708      deps = [
709        {:deps_repo, "0.1.0", path: "custom/deps_repo", env: :test, only: [:dev, :test]},
710        {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo"), only: :test}
711      ]
712
713      with_deps(deps, fn ->
714        in_fixture("deps_status", fn ->
715          loaded = Mix.Dep.load_on_environment([])
716          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
717          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
718
719          loaded = Mix.Dep.load_on_environment(env: :dev)
720          assert [:deps_repo] = Enum.map(loaded, & &1.app)
721          assert [noappfile: {_, _}] = Enum.map(loaded, & &1.status)
722
723          loaded = Mix.Dep.load_on_environment(env: :test)
724          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
725          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
726
727          loaded = Mix.Dep.load_on_environment(env: :prod)
728          assert [] = Enum.map(loaded, & &1.app)
729        end)
730      end)
731    end
732
733    test "converges and diverges when only is not in_upper" do
734      loaded = fn deps ->
735        with_deps(deps, fn ->
736          in_fixture("deps_status", fn ->
737            File.mkdir_p!("custom/other_repo")
738
739            File.write!("custom/other_repo/mix.exs", """
740            defmodule OtherRepo do
741              use Mix.Project
742
743              def project do
744                [app: :other_repo,
745                 version: "0.1.0",
746                 deps: [{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}]]
747              end
748            end
749            """)
750
751            Mix.State.clear_cache()
752            loaded = Mix.Dep.load_on_environment([])
753            Enum.map(loaded, &{&1.app, &1.opts[:only]})
754          end)
755        end)
756      end
757
758      deps = [
759        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
760        {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
761      ]
762
763      assert loaded.(deps) == [git_repo: [:test, :prod], other_repo: :test, deps_repo: :prod]
764
765      deps = [
766        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
767        {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
768      ]
769
770      assert loaded.(deps) == [git_repo: nil, other_repo: :test, deps_repo: nil]
771
772      deps = [
773        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
774        {:other_repo, "0.1.0", path: "custom/other_repo"}
775      ]
776
777      assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: :prod]
778
779      deps = [
780        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
781        {:other_repo, "0.1.0", path: "custom/other_repo"}
782      ]
783
784      assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: nil]
785
786      Process.put(:custom_deps_git_repo_opts, optional: true)
787
788      deps = [
789        {:deps_repo, "0.1.0", path: "custom/deps_repo", only: :prod},
790        {:other_repo, "0.1.0", path: "custom/other_repo", only: :test}
791      ]
792
793      assert loaded.(deps) == [git_repo: :test, other_repo: :test, deps_repo: :prod]
794    end
795
796    test "converges and diverges when only is not specified" do
797      Process.put(:custom_deps_git_repo_opts, only: :test)
798
799      deps = [
800        {:abc_repo, "0.1.0", path: "custom/abc_repo", from_umbrella: true},
801        {:deps_repo, "0.1.0", path: "custom/deps_repo", from_umbrella: true}
802      ]
803
804      with_deps(deps, fn ->
805        in_fixture("deps_status", fn ->
806          File.mkdir_p!("custom/abc_repo")
807
808          File.write!("custom/abc_repo/mix.exs", """
809          defmodule OtherRepo do
810            use Mix.Project
811
812            def project do
813              [app: :abc_repo,
814               version: "0.1.0",
815               deps: [{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}]]
816            end
817          end
818          """)
819
820          Mix.Tasks.Deps.Get.run([])
821          Mix.Tasks.Deps.Compile.run([])
822          refute_receive {:mix_shell, :error, ["Could not compile :git_repo" <> _]}
823        end)
824      end)
825    end
826  end
827
828  describe "targets handling" do
829    test "extracts deps matching target" do
830      deps = [
831        {:foo, github: "elixir-lang/foo"},
832        {:bar, github: "elixir-lang/bar", targets: :rpi3}
833      ]
834
835      with_deps(deps, fn ->
836        in_fixture("deps_status", fn ->
837          deps = Mix.Dep.load_on_environment(target: :rpi3)
838          assert length(deps) == 2
839
840          deps = Mix.Dep.load_on_environment([])
841          assert length(deps) == 2
842
843          assert [dep] = Mix.Dep.load_on_environment(target: :host)
844          assert dep.app == :foo
845        end)
846      end)
847    end
848
849    test "fetches parent deps matching specified target" do
850      deps = [{:target, github: "elixir-lang/target", targets: [:host]}]
851
852      with_deps(deps, fn ->
853        in_fixture("deps_status", fn ->
854          Mix.Tasks.Deps.Get.run(["--target", "rpi3"])
855          refute_received {:mix_shell, :info, ["* Getting" <> _]}
856
857          assert_raise Mix.Error, "Can't continue due to errors on dependencies", fn ->
858            Mix.Tasks.Deps.Loadpaths.run([])
859          end
860
861          Mix.State.clear_cache()
862          Mix.target(:rpi3)
863          Mix.Tasks.Deps.Loadpaths.run([])
864        end)
865      end)
866    end
867
868    test "conflicts on nested deps" do
869      # deps_repo wants all git_repo, git_repo is restricted to targets rpi3
870      deps = [
871        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
872        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), targets: :rpi3}
873      ]
874
875      with_deps(deps, fn ->
876        in_fixture("deps_status", fn ->
877          loaded = Mix.Dep.load_on_environment([])
878          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
879          assert [divergedtargets: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
880
881          loaded = Mix.Dep.load_on_environment(target: :host)
882          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
883          assert [divergedtargets: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
884
885          loaded = Mix.Dep.load_on_environment(target: :rpi3)
886          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
887          assert [divergedtargets: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
888
889          Mix.Tasks.Deps.run([])
890          assert_received {:mix_shell, :info, ["* deps_repo" <> _]}
891          assert_received {:mix_shell, :info, [_]}
892          assert_received {:mix_shell, :info, ["* git_repo" <> _]}
893          assert_received {:mix_shell, :info, [msg]}
894          assert msg =~ "Remove the :targets restriction from your dep"
895        end)
896      end)
897    end
898
899    test "does not conflict with optional deps on nested deps" do
900      Process.put(:custom_deps_git_repo_opts, optional: true)
901
902      # deps_repo wants all git_repo, git_repo is restricted to targets rpi3
903      deps = [
904        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
905        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), targets: :rpi3}
906      ]
907
908      with_deps(deps, fn ->
909        in_fixture("deps_status", fn ->
910          loaded = Mix.Dep.load_on_environment([])
911          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
912          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
913
914          loaded = Mix.Dep.load_on_environment(target: :host)
915          assert [:deps_repo] = Enum.map(loaded, & &1.app)
916          assert [noappfile: {_, _}] = Enum.map(loaded, & &1.status)
917
918          loaded = Mix.Dep.load_on_environment(target: :rpi3)
919          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
920          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
921        end)
922      end)
923    end
924
925    test "does not conflict on valid subsets on nested deps" do
926      # deps_repo wants git_repo for prod, git_repo is restricted to targets bbb and rpi3
927      deps = [
928        {:deps_repo, "0.1.0", path: "custom/deps_repo", targets: :rpi3},
929        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), targets: [:bbb, :rpi3]}
930      ]
931
932      with_deps(deps, fn ->
933        in_fixture("deps_status", fn ->
934          loaded = Mix.Dep.load_on_environment([])
935          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
936          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
937
938          loaded = Mix.Dep.load_on_environment(target: :host)
939          assert [] = Enum.map(loaded, & &1.app)
940
941          loaded = Mix.Dep.load_on_environment(target: :bbb)
942          assert [:git_repo] = Enum.map(loaded, & &1.app)
943          assert [unavailable: _] = Enum.map(loaded, & &1.status)
944
945          loaded = Mix.Dep.load_on_environment(target: :rpi3)
946          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
947          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
948        end)
949      end)
950    end
951
952    test "conflicts on invalid only subset on nested deps" do
953      # deps_repo wants git_repo for rpi3, git_repo is restricted to only test
954      deps = [
955        {:deps_repo, "0.1.0", path: "custom/deps_repo", targets: :host},
956        {:git_repo, "0.2.0", git: MixTest.Case.fixture_path("git_repo"), targets: [:rpi3]}
957      ]
958
959      with_deps(deps, fn ->
960        in_fixture("deps_status", fn ->
961          loaded = Mix.Dep.load_on_environment([])
962          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
963          assert [divergedtargets: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
964
965          loaded = Mix.Dep.load_on_environment(target: :host)
966          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
967          assert [divergedtargets: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
968
969          loaded = Mix.Dep.load_on_environment(target: :rpi3)
970          assert [:git_repo] = Enum.map(loaded, & &1.app)
971          assert [unavailable: _] = Enum.map(loaded, & &1.status)
972
973          Mix.Tasks.Deps.run([])
974          assert_received {:mix_shell, :info, ["* deps_repo" <> _]}
975          assert_received {:mix_shell, :info, [_]}
976          assert_received {:mix_shell, :info, ["* git_repo" <> _]}
977          assert_received {:mix_shell, :info, [msg]}
978          assert msg =~ "Ensure you specify at least the same targets in :targets in your dep"
979        end)
980      end)
981    end
982
983    test "does not conflict with valid only in both parent and child on nested deps" do
984      Process.put(:custom_deps_git_repo_opts, targets: :bbb)
985
986      # deps_repo has environment set to bbb so it loads the deps_git_repo set to bbb too
987      deps = [
988        {:deps_repo, "0.1.0", path: "custom/deps_repo", env: :test, targets: [:host, :bbb]},
989        {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo"), targets: :bbb}
990      ]
991
992      with_deps(deps, fn ->
993        in_fixture("deps_status", fn ->
994          loaded = Mix.Dep.load_on_environment([])
995          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
996          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
997
998          loaded = Mix.Dep.load_on_environment(target: :host)
999          assert [:deps_repo] = Enum.map(loaded, & &1.app)
1000          assert [noappfile: {_, _}] = Enum.map(loaded, & &1.status)
1001
1002          loaded = Mix.Dep.load_on_environment(target: :bbb)
1003          assert [:git_repo, :deps_repo] = Enum.map(loaded, & &1.app)
1004          assert [unavailable: _, noappfile: {_, _}] = Enum.map(loaded, & &1.status)
1005
1006          loaded = Mix.Dep.load_on_environment(target: :rpi3)
1007          assert [] = Enum.map(loaded, & &1.app)
1008        end)
1009      end)
1010    end
1011
1012    test "converges and diverges when only is not in_upper" do
1013      loaded = fn deps ->
1014        with_deps(deps, fn ->
1015          in_fixture("deps_status", fn ->
1016            File.mkdir_p!("custom/other_repo")
1017
1018            File.write!("custom/other_repo/mix.exs", """
1019            defmodule OtherRepo do
1020              use Mix.Project
1021
1022              def project do
1023                [app: :other_repo,
1024                 version: "0.1.0",
1025                 deps: [{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}]]
1026              end
1027            end
1028            """)
1029
1030            Mix.State.clear_cache()
1031            loaded = Mix.Dep.load_on_environment([])
1032            Enum.map(loaded, &{&1.app, &1.opts[:targets]})
1033          end)
1034        end)
1035      end
1036
1037      deps = [
1038        {:deps_repo, "0.1.0", path: "custom/deps_repo", targets: :rpi3},
1039        {:other_repo, "0.1.0", path: "custom/other_repo", targets: :bbb}
1040      ]
1041
1042      assert loaded.(deps) == [git_repo: [:bbb, :rpi3], other_repo: :bbb, deps_repo: :rpi3]
1043
1044      deps = [
1045        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
1046        {:other_repo, "0.1.0", path: "custom/other_repo", targets: :bbb}
1047      ]
1048
1049      assert loaded.(deps) == [git_repo: nil, other_repo: :bbb, deps_repo: nil]
1050
1051      deps = [
1052        {:deps_repo, "0.1.0", path: "custom/deps_repo", targets: :rpi3},
1053        {:other_repo, "0.1.0", path: "custom/other_repo"}
1054      ]
1055
1056      assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: :rpi3]
1057
1058      deps = [
1059        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
1060        {:other_repo, "0.1.0", path: "custom/other_repo"}
1061      ]
1062
1063      assert loaded.(deps) == [git_repo: nil, other_repo: nil, deps_repo: nil]
1064
1065      Process.put(:custom_deps_git_repo_opts, optional: true)
1066
1067      deps = [
1068        {:deps_repo, "0.1.0", path: "custom/deps_repo", targets: :rpi3},
1069        {:other_repo, "0.1.0", path: "custom/other_repo", targets: :bbb}
1070      ]
1071
1072      assert loaded.(deps) == [git_repo: :bbb, other_repo: :bbb, deps_repo: :rpi3]
1073    end
1074  end
1075
1076  describe "overrides" do
1077    test "are not required when there are no conflicts" do
1078      deps = [
1079        {:deps_repo, "0.1.0", path: "custom/deps_repo"}
1080      ]
1081
1082      with_deps(deps, fn ->
1083        in_fixture("deps_status", fn ->
1084          File.mkdir_p!("custom/deps_repo/lib")
1085
1086          File.write!("custom/deps_repo/lib/a.ex", """
1087          # Check that the child dependency is top_level
1088          [%Mix.Dep{app: :git_repo, top_level: true}] = Mix.Dep.cached()
1089          """)
1090
1091          Mix.Tasks.Deps.Get.run([])
1092          Mix.Tasks.Deps.Compile.run([])
1093        end)
1094      end)
1095    end
1096
1097    test "are required when there are conflicts" do
1098      # deps_repo brings git_repo but it is overridden
1099      deps = [
1100        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
1101        {:git_repo, ">= 0.0.0", git: MixTest.Case.fixture_path("git_repo"), override: true}
1102      ]
1103
1104      with_deps(deps, fn ->
1105        in_fixture("deps_status", fn ->
1106          File.mkdir_p!("custom/deps_repo/lib")
1107
1108          File.write!("custom/deps_repo/lib/a.ex", """
1109          # Check that the overridden requirement shows up in the child dependency
1110          [%Mix.Dep{app: :git_repo, requirement: ">= 0.0.0"}] = Mix.Dep.cached()
1111          """)
1112
1113          Mix.Tasks.Deps.Get.run([])
1114          Mix.Tasks.Deps.Compile.run([])
1115        end)
1116      end)
1117    end
1118  end
1119
1120  describe "app generation" do
1121    test "considers runtime from current app on nested deps" do
1122      Process.put(:custom_deps_git_repo_opts, runtime: false)
1123
1124      deps = [
1125        {:deps_repo, "0.1.0", path: "custom/deps_repo"},
1126        {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
1127      ]
1128
1129      with_deps(deps, fn ->
1130        in_fixture("deps_status", fn ->
1131          Mix.Tasks.Deps.Compile.run([])
1132
1133          {:ok, [{:application, :deps_repo, opts}]} =
1134            :file.consult("_build/dev/lib/deps_repo/ebin/deps_repo.app")
1135
1136          assert :git_repo not in Keyword.get(opts, :applications)
1137        end)
1138      end)
1139    end
1140
1141    test "considers only from current app on nested deps" do
1142      Process.put(:custom_deps_git_repo_opts, only: :other)
1143
1144      deps = [
1145        {:deps_repo, "0.1.0", path: "custom/deps_repo", from_umbrella: true},
1146        {:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo"), from_umbrella: true}
1147      ]
1148
1149      with_deps(deps, fn ->
1150        in_fixture("deps_status", fn ->
1151          Mix.Tasks.Deps.Compile.run([])
1152
1153          {:ok, [{:application, :deps_repo, opts}]} =
1154            :file.consult("_build/dev/lib/deps_repo/ebin/deps_repo.app")
1155
1156          assert :git_repo not in Keyword.get(opts, :applications)
1157        end)
1158      end)
1159    end
1160  end
1161end
1162