1Code.require_file("../../test_helper.exs", __DIR__)
2
3defmodule Mix.Tasks.Compile.ProtocolsTest do
4  use MixTest.Case
5
6  @old {{2010, 1, 1}, {0, 0, 0}}
7
8  test "compiles and consolidates local protocols", context do
9    in_tmp(context.test, fn ->
10      Mix.Project.push(MixTest.Case.Sample)
11
12      File.mkdir_p!("lib")
13      assert Mix.Task.run("compile")
14
15      # Define a local protocol
16      File.write!("lib/protocol.ex", """
17      defprotocol Compile.Protocol do
18        def foo(a, b)
19      end
20      """)
21
22      assert compile_elixir_and_protocols() == :ok
23      mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
24
25      # Implement a local protocol
26      File.write!("lib/impl.ex", """
27      defimpl Compile.Protocol, for: Integer do
28        def foo(a, b), do: a + b
29      end
30      """)
31
32      assert compile_elixir_and_protocols() == :ok
33
34      assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") !=
35               @old
36
37      # Delete a local implementation
38      File.rm!("lib/impl.ex")
39      assert compile_elixir_and_protocols() == :ok
40
41      assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam") !=
42               @old
43
44      # Delete a local protocol
45      File.rm!("lib/protocol.ex")
46      assert compile_elixir_and_protocols() == :noop
47      refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
48    end)
49  end
50
51  test "compiles after converting a protocol into a standard module", context do
52    in_tmp(context.test, fn ->
53      Mix.Project.push(MixTest.Case.Sample)
54
55      File.mkdir_p!("lib")
56      Mix.Task.run("compile")
57      purge_protocol(Compile.Protocol)
58
59      # Define a local protocol
60      File.write!("lib/protocol.ex", """
61      defprotocol Compile.Protocol do
62        def foo(a)
63      end
64
65      defimpl Compile.Protocol, for: Integer do
66        def foo(a), do: a
67      end
68      """)
69
70      assert compile_elixir_and_protocols() == :ok
71      mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
72      File.rm!("lib/protocol.ex")
73
74      # Define a standard module
75      File.write!("lib/protocol.ex", """
76      defmodule Compile.Protocol do
77      end
78      """)
79
80      assert compile_elixir_and_protocols() == :noop
81
82      # Delete a local protocol
83      File.rm!("lib/protocol.ex")
84      assert compile_elixir_and_protocols() == :noop
85      refute File.regular?("_build/dev/lib/sample/consolidated/Elixir.Compile.Protocol.beam")
86    end)
87  end
88
89  test "compiles and consolidates deps protocols", context do
90    in_tmp(context.test, fn ->
91      Mix.Project.push(MixTest.Case.Sample)
92
93      File.mkdir_p!("lib")
94      Mix.Task.run("compile")
95      purge_protocol(String.Chars)
96      mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam")
97
98      assert compile_elixir_and_protocols() == :noop
99      assert mtime("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") == @old
100
101      # Implement a deps protocol
102      File.write!("lib/struct.ex", """
103      defmodule Compile.Protocol.Struct do
104        defstruct a: nil
105        defimpl String.Chars do
106          def to_string(_), do: "ok"
107        end
108      end
109      """)
110
111      assert compile_elixir_and_protocols() == :ok
112      assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != @old
113
114      # Delete the local implementation
115      File.rm!("lib/struct.ex")
116      assert compile_elixir_and_protocols() == :ok
117      assert mark_as_old!("_build/dev/lib/sample/consolidated/Elixir.String.Chars.beam") != @old
118    end)
119  end
120
121  test "consolidated protocols keep relative path to their source" do
122    in_fixture("no_mixfile", fn ->
123      Mix.Project.push(MixTest.Case.Sample)
124      compile_elixir_and_protocols()
125
126      # Load consolidated
127      :code.add_patha('_build/dev/lib/sample/consolidated')
128      :code.purge(Enumerable)
129      :code.delete(Enumerable)
130
131      try do
132        Enumerable.impl_for!(:oops)
133      rescue
134        Protocol.UndefinedError ->
135          assert [{_, _, _, [file: 'lib/enum.ex'] ++ _} | _] = __STACKTRACE__
136      else
137        _ ->
138          flunk("Enumerable.impl_for!/1 should have failed")
139      after
140        purge_protocol(Enumerable)
141      end
142    end)
143  end
144
145  defp compile_elixir_and_protocols do
146    Mix.Tasks.Compile.Elixir.run([])
147    Mix.Tasks.Compile.Protocols.run([])
148  end
149
150  defp mtime(path) do
151    File.stat!(path).mtime
152  end
153
154  defp mark_as_old!(path) do
155    mtime = mtime(path)
156    File.touch!(path, @old)
157    mtime
158  end
159
160  defp purge_protocol(module) do
161    :code.del_path(:filename.absname('_build/dev/lib/sample/consolidated'))
162    :code.purge(module)
163    :code.delete(module)
164  end
165end
166