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