1Code.require_file("../../test_helper.exs", __DIR__)
2
3defmodule Mix.Tasks.ArchiveTest do
4  use MixTest.Case
5
6  defmodule ArchiveProject do
7    def project do
8      [app: :archive, version: "0.1.0", elixir: "~> 0.1.0"]
9    end
10  end
11
12  defmodule ArchiveProject2 do
13    def project do
14      [app: :archive, version: "0.2.0"]
15    end
16  end
17
18  setup do
19    File.rm_rf!(tmp_path("userhome"))
20    System.put_env("MIX_ARCHIVES", tmp_path("userhome/.mix/archives/"))
21    Mix.Project.push(ArchiveProject)
22
23    on_exit(fn ->
24      Mix.Local.remove_archives()
25      System.delete_env("MIX_ARCHIVES")
26    end)
27  end
28
29  test "archive build" do
30    in_fixture("archive", fn ->
31      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check"])
32      message = "Generated archive \"archive-0.1.0.ez\" with MIX_ENV=dev"
33      assert_received {:mix_shell, :info, [^message]}
34      assert File.regular?('archive-0.1.0.ez')
35
36      assert_archive_content_default()
37      refute has_in_zip_file?('archive-0.1.0.ez', 'archive-0.1.0/priv/.dot_file')
38    end)
39  end
40
41  test "archive build with include-dot-files" do
42    in_fixture("archive", fn ->
43      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check", "--include-dot-files"])
44      message = "Generated archive \"archive-0.1.0.ez\" with MIX_ENV=dev"
45      assert_received {:mix_shell, :info, [^message]}
46      assert File.regular?('archive-0.1.0.ez')
47
48      assert_archive_content_default()
49      assert has_in_zip_file?('archive-0.1.0.ez', 'archive-0.1.0/priv/.dot_file')
50    end)
51  end
52
53  def assert_archive_content_default() do
54    assert File.regular?('archive-0.1.0.ez')
55    assert has_in_zip_file?('archive-0.1.0.ez', 'archive-0.1.0/.elixir')
56    assert has_in_zip_file?('archive-0.1.0.ez', 'archive-0.1.0/priv/not_really_an.so')
57
58    assert has_in_zip_file?(
59             'archive-0.1.0.ez',
60             'archive-0.1.0/ebin/Elixir.Mix.Tasks.Local.Sample.beam'
61           )
62
63    assert has_in_zip_file?('archive-0.1.0.ez', 'archive-0.1.0/ebin/archive.app')
64  end
65
66  test "archive install" do
67    in_fixture("archive", fn ->
68      # Build and install archive
69      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check"])
70
71      send(self(), {:mix_shell_input, :yes?, true})
72      Mix.Tasks.Archive.Install.run([])
73      refute File.regular?(tmp_path("userhome/.mix/archives/archive-0.1.0.ez"))
74      assert File.dir?(tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin"))
75
76      # Check that the version warning is printed after installation
77      version_error =
78        "warning: the archive archive-0.1.0 requires Elixir \"~> 0.1.0\" " <>
79          "but you are running on v#{System.version()}"
80
81      assert_received {:mix_shell, :error, [^version_error]}
82
83      archive = tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin")
84      assert to_charlist(archive) in :code.get_path()
85
86      # Loading the archive should emit warning again
87      Mix.Local.append_archives()
88      assert_received {:mix_shell, :error, [^version_error]}
89
90      # List archive
91      Mix.Tasks.Local.run([])
92      info = "mix local.sample # A local install sample"
93      assert_received {:mix_shell, :info, [^info]}
94
95      Mix.Tasks.Archive.run([])
96      assert_received {:mix_shell, :info, ["* archive-0.1.0"]}
97
98      # Run archived task
99      Mix.Task.run("local.sample")
100      assert_received {:mix_shell, :info, ["sample"]}
101    end)
102  end
103
104  test "archive install invalid file" do
105    in_fixture("archive", fn ->
106      file_name = "invalid-archive-0.1.0.ez"
107      assert File.regular?(file_name)
108
109      send(self(), {:mix_shell_input, :yes?, true})
110
111      assert_raise Mix.Error, ~r/invalid archive file/, fn ->
112        Mix.Tasks.Archive.Install.run([file_name])
113      end
114    end)
115  end
116
117  test "archive install missing file" do
118    message = ~r[Expected "./unlikely-to-exist-0.1.0.ez" to be a local file path]
119
120    assert_raise Mix.Error, message, fn ->
121      Mix.Tasks.Archive.Install.run(["./unlikely-to-exist-0.1.0.ez"])
122    end
123  end
124
125  test "archive install --force" do
126    in_fixture("archive", fn ->
127      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check"])
128      Mix.Tasks.Archive.Install.run(["--force"])
129
130      message = "Generated archive \"archive-0.1.0.ez\" with MIX_ENV=dev"
131      assert_received {:mix_shell, :info, [^message]}
132
133      Mix.Tasks.Archive.Uninstall.run(["archive-0.1.0", "--force"])
134      refute File.dir?(tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin"))
135    end)
136  end
137
138  @compile {:no_warn_undefined, GitRepo.Archive}
139  test "archive.install from Git" do
140    in_fixture("git_repo", fn ->
141      File.mkdir_p!("config")
142
143      File.write!("config/config.exs", """
144      import Config
145      config :git_repo, :archive_config, true
146      """)
147
148      File.write!("lib/git_repo.ex", """
149      require Application
150      true = Application.compile_env!(:git_repo, :archive_config)
151
152      defmodule GitRepo.Archive do
153        def hello do
154          "World"
155        end
156      end
157      """)
158
159      System.cmd("git", ~w[init])
160      System.cmd("git", ~w[add .])
161      System.cmd("git", ~w[commit -m first-commit])
162
163      send(self(), {:mix_shell_input, :yes?, true})
164      Mix.Tasks.Archive.Install.run(["git", File.cwd!()])
165      assert GitRepo.Archive.hello() == "World"
166
167      message = "Generated archive \"git_repo-0.1.0.ez\" with MIX_ENV=prod"
168      assert_received {:mix_shell, :info, [^message]}
169
170      refute File.regular?(tmp_path("userhome/.mix/archives/git_repo-0.1.0.ez"))
171      assert File.dir?(tmp_path("userhome/.mix/archives/git_repo-0.1.0/git_repo-0.1.0/ebin"))
172    end)
173  after
174    purge([GitRepo.Archive, GitRepo.MixProject])
175  end
176
177  test "archive install, update, and uninstall life-cycle" do
178    in_fixture("archive", fn ->
179      # Install previous version
180      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check"])
181      assert File.regular?("archive-0.1.0.ez")
182      send(self(), {:mix_shell_input, :yes?, true})
183      Mix.Tasks.Archive.Install.run([])
184      assert_received {:mix_shell, :error, [_]}
185
186      # Build new version
187      Mix.Project.push(ArchiveProject2)
188      Mix.Tasks.Archive.Build.run(["--no-compile"])
189      assert File.regular?("archive-0.2.0.ez")
190
191      message = "Generated archive \"archive-0.2.0.ez\" with MIX_ENV=dev"
192      assert_received {:mix_shell, :info, [^message]}
193
194      # Install new version
195      send(self(), {:mix_shell_input, :yes?, true})
196      Mix.Tasks.Archive.Install.run([])
197      refute File.regular?(tmp_path("userhome/.mix/archives/archive-0.2.0.ez"))
198      assert File.dir?(tmp_path("userhome/.mix/archives/archive-0.2.0/archive-0.2.0/ebin"))
199
200      # Re-install current version should not change system
201      send(self(), {:mix_shell_input, :yes?, true})
202      Mix.Tasks.Archive.Install.run([])
203      refute File.regular?(tmp_path("userhome/.mix/archives/archive-0.2.0.ez"))
204      assert File.dir?(tmp_path("userhome/.mix/archives/archive-0.2.0/archive-0.2.0/ebin"))
205
206      # Try to install a missing version does not remove archive
207      assert_raise Mix.Error, fn ->
208        Mix.Tasks.Archive.Install.run(["./archive-0.0.0.ez"])
209      end
210
211      assert File.dir?(tmp_path("userhome/.mix/archives/archive-0.2.0/archive-0.2.0/ebin"))
212      refute File.regular?(tmp_path("userhome/.mix/archives/archive-0.1.0.ez"))
213
214      # Load archive without warnings because there is no :elixir requirement in mix.exs
215      Mix.Local.append_archives()
216      refute_received {:mix_shell, :error, [_]}
217
218      # Check uninstall confirmation
219      send(self(), {:mix_shell_input, :yes?, false})
220      Mix.Tasks.Archive.Uninstall.run(["archive-0.2.0"])
221      assert File.dir?(tmp_path("userhome/.mix/archives/archive-0.2.0/archive-0.2.0/ebin"))
222
223      # Remove it!
224      send(self(), {:mix_shell_input, :yes?, true})
225      Mix.Tasks.Archive.Uninstall.run(["archive-0.2.0"])
226      refute File.dir?(tmp_path("userhome/.mix/archives/archive-0.2.0/archive-0.2.0/ebin"))
227
228      # Check old paths are unloaded
229      paths = Enum.map(:code.get_path(), &List.to_string/1)
230      refute tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin") in paths
231    end)
232  end
233
234  test "archive uninstall without version" do
235    in_fixture("archive", fn ->
236      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check"])
237      send(self(), {:mix_shell_input, :yes?, true})
238      Mix.Tasks.Archive.Install.run([])
239
240      send(self(), {:mix_shell_input, :yes?, false})
241      Mix.Tasks.Archive.Uninstall.run(["archive"])
242      assert File.dir?(tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin"))
243
244      send(self(), {:mix_shell_input, :yes?, true})
245      Mix.Tasks.Archive.Uninstall.run(["archive"])
246      refute File.dir?(tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin"))
247    end)
248  end
249
250  test "archive checksum" do
251    in_fixture("archive", fn ->
252      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check"])
253      assert File.regular?("archive-0.1.0.ez")
254      send(self(), {:mix_shell_input, :yes?, true})
255
256      # Install with wrong checksum
257      assert_raise Mix.Error, ~r"Data does not match the given SHA-512 checksum", fn ->
258        send(self(), {:mix_shell_input, :yes?, true})
259        Mix.Tasks.Archive.Install.run(["--sha512", "wrong"])
260      end
261
262      # Install with correct checksum
263      send(self(), {:mix_shell_input, :yes?, true})
264      Mix.Tasks.Archive.Install.run(["--sha512", sha512("archive-0.1.0.ez")])
265      refute File.regular?(tmp_path("userhome/.mix/archives/archive-0.1.0.ez"))
266      assert File.dir?(tmp_path("userhome/.mix/archives/archive-0.1.0/archive-0.1.0/ebin"))
267    end)
268  end
269
270  test "archive check" do
271    # Install the archive
272    in_fixture("archive", fn ->
273      Mix.Tasks.Archive.Build.run(["--no-elixir-version-check"])
274      send(self(), {:mix_shell_input, :yes?, true})
275      Mix.Tasks.Archive.Install.run([])
276      Application.unload(:archive)
277    end)
278
279    assert_raise Mix.Error, ~r/Expected archive to be in the format/, fn ->
280      archive_check([:archive])
281    end
282
283    assert_raise Mix.Error, ~r/Archive "archive" could not be found/, fn ->
284      archive_check([{:archive, ">= 1.0.0"}])
285    end
286
287    # Load the archive
288    Mix.Local.append_archives()
289
290    message = ~r/Archive \"archive-0.1.0\" does not match requirement >= 1.0.0/
291
292    assert_raise Mix.Error, message, fn ->
293      archive_check([{:archive, ">= 1.0.0"}])
294    end
295
296    archive_check([{:archive, ">= 0.0.0"}])
297  end
298
299  defp archive_check(archives) do
300    Mix.Project.pop()
301    Mix.ProjectStack.post_config(archives: archives)
302    Mix.Project.push(MixTest.Case.Sample)
303    Mix.Tasks.Archive.Check.run([])
304  end
305
306  defp sha512(file) do
307    Base.encode16(:crypto.hash(:sha512, File.read!(file)), case: :lower)
308  end
309
310  defp has_in_zip_file?(archive, name) do
311    {:ok, files} = :zip.list_dir(archive)
312    Enum.find(files, &match?({:zip_file, ^name, _, _, _, _}, &1))
313  end
314end
315