1Code.require_file("test_helper.exs", __DIR__)
2
3defmodule ProtocolTest do
4  use ExUnit.Case, async: true
5
6  doctest Protocol
7
8  {_, _, sample_binary, _} =
9    defprotocol Sample do
10      @type t :: any
11      @doc "Ok"
12      @deprecated "Reason"
13      @spec ok(t) :: boolean
14      def ok(term)
15    end
16
17  @sample_binary sample_binary
18
19  {_, _, with_any_binary, _} =
20    defprotocol WithAny do
21      @fallback_to_any true
22      @doc "Ok"
23      def ok(term)
24    end
25
26  @with_any_binary with_any_binary
27
28  defprotocol Derivable do
29    def ok(a)
30  end
31
32  defimpl Derivable, for: Any do
33    defmacro __deriving__(module, struct, options) do
34      quote do
35        defimpl Derivable, for: unquote(module) do
36          def ok(arg) do
37            {:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
38          end
39        end
40      end
41    end
42
43    def ok(arg) do
44      {:ok, arg}
45    end
46  end
47
48  defimpl WithAny, for: Map do
49    def ok(map) do
50      {:ok, map}
51    end
52  end
53
54  defimpl WithAny, for: Any do
55    def ok(any) do
56      {:ok, any}
57    end
58  end
59
60  defmodule NoImplStruct do
61    defstruct a: 0, b: 0
62  end
63
64  defmodule ImplStruct do
65    @derive [WithAny, Derivable]
66    defstruct a: 0, b: 0
67
68    defimpl Sample do
69      @compile {:no_warn_undefined, Unknown}
70
71      def ok(struct) do
72        Unknown.undefined(struct)
73      end
74    end
75  end
76
77  test "protocol implementations without any" do
78    assert is_nil(Sample.impl_for(:foo))
79    assert is_nil(Sample.impl_for(fn x -> x end))
80    assert is_nil(Sample.impl_for(1))
81    assert is_nil(Sample.impl_for(1.1))
82    assert is_nil(Sample.impl_for([]))
83    assert is_nil(Sample.impl_for([1, 2, 3]))
84    assert is_nil(Sample.impl_for({}))
85    assert is_nil(Sample.impl_for({1, 2, 3}))
86    assert is_nil(Sample.impl_for("foo"))
87    assert is_nil(Sample.impl_for(<<1>>))
88    assert is_nil(Sample.impl_for(%{}))
89    assert is_nil(Sample.impl_for(self()))
90    assert is_nil(Sample.impl_for(hd(:erlang.ports())))
91    assert is_nil(Sample.impl_for(make_ref()))
92
93    assert Sample.impl_for(%ImplStruct{}) == Sample.ProtocolTest.ImplStruct
94    assert Sample.impl_for(%NoImplStruct{}) == nil
95  end
96
97  test "protocol implementation with Any and struct fallbacks" do
98    assert WithAny.impl_for(%NoImplStruct{}) == WithAny.Any
99    # Derived
100    assert WithAny.impl_for(%ImplStruct{}) == WithAny.Any
101    assert WithAny.impl_for(%{__struct__: "foo"}) == WithAny.Map
102    assert WithAny.impl_for(%{}) == WithAny.Map
103    assert WithAny.impl_for(self()) == WithAny.Any
104  end
105
106  test "protocol not implemented" do
107    message = "protocol ProtocolTest.Sample not implemented for :foo of type Atom"
108
109    assert_raise Protocol.UndefinedError, message, fn ->
110      sample = Sample
111      sample.ok(:foo)
112    end
113  end
114
115  test "protocol documentation and deprecated" do
116    import PathHelpers
117
118    write_beam(
119      defprotocol SampleDocsProto do
120        @type t :: any
121        @doc "Ok"
122        @deprecated "Reason"
123        @spec ok(t) :: boolean
124        def ok(term)
125      end
126    )
127
128    {:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(SampleDocsProto)
129
130    assert {{:function, :ok, 1}, _, ["ok(term)"], %{"en" => "Ok"}, _} =
131             List.keyfind(docs, {:function, :ok, 1}, 0)
132
133    deprecated = SampleDocsProto.__info__(:deprecated)
134    assert [{{:ok, 1}, "Reason"}] = deprecated
135  end
136
137  @compile {:no_warn_undefined, WithAll}
138
139  test "protocol keeps underlying UndefinedFunctionError" do
140    assert_raise UndefinedFunctionError, fn ->
141      WithAll.ok(%ImplStruct{})
142    end
143  end
144
145  test "protocol defines callbacks" do
146    assert [{:type, 13, :fun, args}] = get_callbacks(@sample_binary, :ok, 1)
147    assert args == [{:type, 13, :product, [{:user_type, 13, :t, []}]}, {:type, 13, :boolean, []}]
148
149    assert [{:type, 23, :fun, args}] = get_callbacks(@with_any_binary, :ok, 1)
150    assert args == [{:type, 23, :product, [{:user_type, 23, :t, []}]}, {:type, 23, :term, []}]
151  end
152
153  test "protocol defines functions and attributes" do
154    assert Sample.__protocol__(:module) == Sample
155    assert Sample.__protocol__(:functions) == [ok: 1]
156    refute Sample.__protocol__(:consolidated?)
157    assert Sample.__protocol__(:impls) == :not_consolidated
158    assert Sample.__info__(:attributes)[:__protocol__] == [fallback_to_any: false]
159
160    assert WithAny.__protocol__(:module) == WithAny
161    assert WithAny.__protocol__(:functions) == [ok: 1]
162    refute WithAny.__protocol__(:consolidated?)
163    assert WithAny.__protocol__(:impls) == :not_consolidated
164    assert WithAny.__info__(:attributes)[:__protocol__] == [fallback_to_any: true]
165  end
166
167  test "defimpl" do
168    module = Module.concat(Sample, ImplStruct)
169    assert module.__impl__(:for) == ImplStruct
170    assert module.__impl__(:target) == module
171    assert module.__impl__(:protocol) == Sample
172    assert module.__info__(:attributes)[:__impl__] == [protocol: Sample, for: ImplStruct]
173  end
174
175  test "defimpl with implicit derive" do
176    module = Module.concat(WithAny, ImplStruct)
177    assert module.__impl__(:for) == ImplStruct
178    assert module.__impl__(:target) == WithAny.Any
179    assert module.__impl__(:protocol) == WithAny
180    assert module.__info__(:attributes)[:__impl__] == [protocol: WithAny, for: ImplStruct]
181  end
182
183  test "defimpl with explicit derive" do
184    module = Module.concat(Derivable, ImplStruct)
185    assert module.__impl__(:for) == ImplStruct
186    assert module.__impl__(:target) == module
187    assert module.__impl__(:protocol) == Derivable
188    assert module.__info__(:attributes)[:__impl__] == [protocol: Derivable, for: ImplStruct]
189  end
190
191  test "defimpl with multiple for" do
192    defprotocol Multi do
193      def test(a)
194    end
195
196    defimpl Multi, for: [Atom, Integer] do
197      def test(a), do: a
198    end
199
200    assert Multi.test(1) == 1
201    assert Multi.test(:a) == :a
202  end
203
204  test "defimpl without :for option when outside a module" do
205    msg = "defimpl/3 expects a :for option when declared outside a module"
206
207    assert_raise ArgumentError, msg, fn ->
208      ast =
209        quote do
210          defimpl Sample do
211            def ok(_term), do: true
212          end
213        end
214
215      Code.eval_quoted(ast, [], %{__ENV__ | module: nil})
216    end
217  end
218
219  defp get_callbacks(beam, name, arity) do
220    {:ok, callbacks} = Code.Typespec.fetch_callbacks(beam)
221    List.keyfind(callbacks, {name, arity}, 0) |> elem(1)
222  end
223
224  test "derives protocol implicitly" do
225    struct = %ImplStruct{a: 1, b: 1}
226    assert WithAny.ok(struct) == {:ok, struct}
227
228    struct = %NoImplStruct{a: 1, b: 1}
229    assert WithAny.ok(struct) == {:ok, struct}
230  end
231
232  test "derives protocol explicitly" do
233    struct = %ImplStruct{a: 1, b: 1}
234    assert Derivable.ok(struct) == {:ok, struct, %ImplStruct{}, []}
235
236    assert_raise Protocol.UndefinedError, fn ->
237      struct = %NoImplStruct{a: 1, b: 1}
238      Derivable.ok(struct)
239    end
240  end
241
242  test "derives protocol explicitly with options" do
243    defmodule AnotherStruct do
244      @derive [{Derivable, :ok}]
245      @derive [WithAny]
246      defstruct a: 0, b: 0
247    end
248
249    struct = struct(AnotherStruct, a: 1, b: 1)
250    assert Derivable.ok(struct) == {:ok, struct, struct(AnotherStruct), :ok}
251  end
252
253  test "derive protocol explicitly via API" do
254    defmodule InlineStruct do
255      defstruct a: 0, b: 0
256    end
257
258    require Protocol
259    assert Protocol.derive(Derivable, InlineStruct, :oops) == :ok
260
261    struct = struct(InlineStruct, a: 1, b: 1)
262    assert Derivable.ok(struct) == {:ok, struct, struct(InlineStruct), :oops}
263  end
264
265  test "derived implementation keeps local file/line info" do
266    assert ProtocolTest.WithAny.ProtocolTest.ImplStruct.__info__(:compile)[:source] ==
267             String.to_charlist(__ENV__.file)
268  end
269
270  test "cannot derive without any implementation" do
271    assert_raise ArgumentError,
272                 ~r"could not load module #{inspect(Sample.Any)} due to reason :nofile, cannot derive #{inspect(Sample)}",
273                 fn ->
274                   defmodule NotCompiled do
275                     @derive [Sample]
276                     defstruct hello: :world
277                   end
278                 end
279  end
280
281  test "malformed @callback raises with CompileError" do
282    assert_raise CompileError,
283                 "nofile:2: type specification missing return type: foo(term)",
284                 fn ->
285                   Code.eval_string("""
286                   defprotocol WithMalformedCallback do
287                     @callback foo(term)
288                   end
289                   """)
290                 end
291  end
292end
293
294defmodule Protocol.DebugInfoTest do
295  use ExUnit.Case
296
297  test "protocols always keep debug_info" do
298    Code.compiler_options(debug_info: false)
299
300    {:module, _, binary, _} =
301      defprotocol DebugInfoProto do
302      end
303
304    assert {:ok, {DebugInfoProto, [debug_info: debug_info]}} =
305             :beam_lib.chunks(binary, [:debug_info])
306
307    assert {:debug_info_v1, :elixir_erl, {:elixir_v1, _, _}} = debug_info
308  after
309    Code.compiler_options(debug_info: true)
310  end
311end
312