Skip to content

Commit 965505f

Browse files
committed
Expand imports within describe, closes #11726
1 parent 27c3a4a commit 965505f

File tree

3 files changed

+191
-88
lines changed

3 files changed

+191
-88
lines changed

lib/ex_unit/lib/ex_unit/callbacks.ex

Lines changed: 162 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ defmodule ExUnit.Callbacks do
167167
@ex_unit_describe nil
168168
@ex_unit_setup []
169169
@ex_unit_setup_all []
170+
@ex_unit_used_describes %{}
170171

171172
@before_compile unquote(__MODULE__)
172173
import unquote(__MODULE__)
@@ -175,7 +176,8 @@ defmodule ExUnit.Callbacks do
175176

176177
@doc false
177178
defmacro __before_compile__(env) do
178-
[compile_callbacks(env, :setup), compile_callbacks(env, :setup_all)]
179+
used_describes = Module.get_attribute(env.module, :ex_unit_used_describes)
180+
[compile_setup(env, :setup, used_describes), compile_setup(env, :setup_all, %{})]
179181
end
180182

181183
@doc """
@@ -208,8 +210,7 @@ defmodule ExUnit.Callbacks do
208210
do_setup(quote(do: _), block)
209211
else
210212
quote do
211-
@ex_unit_setup ExUnit.Callbacks.__callback__(unquote(block), @ex_unit_describe) ++
212-
@ex_unit_setup
213+
ExUnit.Callbacks.__setup__(__MODULE__, unquote(block))
213214
end
214215
end
215216
end
@@ -236,12 +237,31 @@ defmodule ExUnit.Callbacks do
236237

237238
defp do_setup(context, block) do
238239
quote bind_quoted: [context: escape(context), block: escape(block)] do
239-
name = :"__ex_unit_setup_#{length(@ex_unit_setup)}"
240+
name = ExUnit.Callbacks.__setup__(__MODULE__)
240241
defp unquote(name)(unquote(context)), unquote(block)
241-
@ex_unit_setup [{name, @ex_unit_describe} | @ex_unit_setup]
242242
end
243243
end
244244

245+
@doc false
246+
def __setup__(module, callbacks) do
247+
setup = Module.get_attribute(module, :ex_unit_setup)
248+
Module.put_attribute(module, :ex_unit_setup, Enum.reverse(callbacks(callbacks), setup))
249+
end
250+
251+
@doc false
252+
def __setup__(module) do
253+
setup = Module.get_attribute(module, :ex_unit_setup)
254+
255+
name =
256+
case Module.get_attribute(module, :ex_unit_describe) do
257+
{_line, _message, counter} -> :"__ex_unit_setup_#{counter}_#{length(setup)}"
258+
nil -> :"__ex_unit_setup_#{length(setup)}"
259+
end
260+
261+
Module.put_attribute(module, :ex_unit_setup, [name | setup])
262+
name
263+
end
264+
245265
@doc """
246266
Defines a callback to be run before all tests in a case.
247267
@@ -283,12 +303,7 @@ defmodule ExUnit.Callbacks do
283303
do_setup_all(quote(do: _), block)
284304
else
285305
quote do
286-
@ex_unit_describe &&
287-
raise "cannot invoke setup_all/1 inside describe as setup_all/1 " <>
288-
"always applies to all tests in a module"
289-
290-
@ex_unit_setup_all ExUnit.Callbacks.__callback__(unquote(block), nil) ++
291-
@ex_unit_setup_all
306+
ExUnit.Callbacks.__setup_all__(__MODULE__, unquote(block))
292307
end
293308
end
294309
end
@@ -312,10 +327,48 @@ defmodule ExUnit.Callbacks do
312327

313328
defp do_setup_all(context, block) do
314329
quote bind_quoted: [context: escape(context), block: escape(block)] do
315-
@ex_unit_describe && raise "cannot invoke setup_all/2 inside describe"
316-
name = :"__ex_unit_setup_all_#{length(@ex_unit_setup_all)}"
330+
name = ExUnit.Callbacks.__setup_all__(__MODULE__)
317331
defp unquote(name)(unquote(context)), unquote(block)
318-
@ex_unit_setup_all [{name, nil} | @ex_unit_setup_all]
332+
end
333+
end
334+
335+
@doc false
336+
def __setup_all__(module, callbacks) do
337+
no_describe!(module)
338+
setup_all = Module.get_attribute(module, :ex_unit_setup_all)
339+
340+
Module.put_attribute(
341+
module,
342+
:ex_unit_setup_all,
343+
Enum.reverse(callbacks(callbacks), setup_all)
344+
)
345+
end
346+
347+
@doc false
348+
def __setup_all__(module) do
349+
no_describe!(module)
350+
setup_all = Module.get_attribute(module, :ex_unit_setup_all)
351+
name = :"__ex_unit_setup_all_#{length(setup_all)}"
352+
Module.put_attribute(module, :ex_unit_setup_all, [name | setup_all])
353+
name
354+
end
355+
356+
defp no_describe!(module) do
357+
if Module.get_attribute(module, :ex_unit_describe) do
358+
raise "cannot invoke setup_all/1-2 inside describe as setup_all " <>
359+
"always applies to all tests in a module"
360+
end
361+
end
362+
363+
defp callbacks(callbacks) do
364+
for k <- List.wrap(callbacks) do
365+
if not is_atom(k) do
366+
raise ArgumentError,
367+
"setup/setup_all expect a callback name as an atom or " <>
368+
"a list of callback names, got: #{inspect(k)}"
369+
end
370+
371+
k
319372
end
320373
end
321374

@@ -558,8 +611,8 @@ defmodule ExUnit.Callbacks do
558611
raise_merge_failed!(mod, original_value)
559612
end
560613

561-
defp merge(mod, context, data, original_value) when is_list(data) do
562-
merge(mod, context, Map.new(data), original_value)
614+
defp merge(mod, context, data, _original_value) when is_list(data) do
615+
context_merge(mod, context, Map.new(data))
563616
end
564617

565618
defp merge(mod, context, data, _original_value) when is_map(data) do
@@ -594,44 +647,112 @@ defmodule ExUnit.Callbacks do
594647
Macro.escape(contents, unquote: true)
595648
end
596649

597-
defp compile_callbacks(env, kind) do
598-
callbacks = Module.get_attribute(env.module, :"ex_unit_#{kind}") |> Enum.reverse()
650+
@doc false
651+
def __describe__(module, line, message, fun) do
652+
if Module.get_attribute(module, :ex_unit_describe) do
653+
raise "cannot call \"describe\" inside another \"describe\". See the documentation " <>
654+
"for ExUnit.Case.describe/2 on named setups and how to handle hierarchies"
655+
end
656+
657+
used_describes = Module.get_attribute(module, :ex_unit_used_describes)
658+
659+
cond do
660+
not is_binary(message) ->
661+
raise ArgumentError, "describe name must be a string, got: #{inspect(message)}"
599662

600-
acc =
601-
case callbacks do
602-
[] ->
603-
quote(do: context)
663+
is_map_key(used_describes, message) ->
664+
raise ExUnit.DuplicateDescribeError,
665+
"describe #{inspect(message)} is already defined in #{inspect(module)}"
604666

605-
[h | t] ->
606-
Enum.reduce(t, compile_merge(h), fn callback_describe, acc ->
667+
true ->
668+
:ok
669+
end
670+
671+
if Module.get_attribute(module, :describetag) != [] do
672+
raise "@describetag must be set inside describe/2 blocks"
673+
end
674+
675+
setup = Module.get_attribute(module, :ex_unit_setup)
676+
Module.put_attribute(module, :ex_unit_setup, [])
677+
Module.put_attribute(module, :ex_unit_describe, {line, message, map_size(used_describes)})
678+
679+
try do
680+
fun.(message, used_describes)
681+
after
682+
Module.put_attribute(module, :ex_unit_describe, nil)
683+
Module.put_attribute(module, :ex_unit_setup, setup)
684+
Module.delete_attribute(module, :describetag)
685+
686+
for attribute <- Module.get_attribute(module, :ex_unit_registered_describe_attributes) do
687+
Module.delete_attribute(module, attribute)
688+
end
689+
end
690+
end
691+
692+
@doc false
693+
def __describe__(module, message, used_describes) do
694+
{name, body} =
695+
case Module.get_attribute(module, :ex_unit_setup) do
696+
[] -> {nil, nil}
697+
callbacks -> {:"__ex_unit_describe_#{map_size(used_describes)}", compile_setup(callbacks)}
698+
end
699+
700+
used_describes = Map.put(used_describes, message, name)
701+
Module.put_attribute(module, :ex_unit_used_describes, used_describes)
702+
{name, body}
703+
end
704+
705+
defp compile_setup(env, kind, describes) do
706+
calls =
707+
env.module
708+
|> Module.get_attribute(:"ex_unit_#{kind}")
709+
|> compile_setup()
710+
711+
describe_clauses =
712+
for {describe, callback} <- describes,
713+
callback != nil,
714+
clause <- quote(do: (unquote(describe) -> unquote(callback)(var!(context)))),
715+
do: clause
716+
717+
body =
718+
if describe_clauses == [] do
719+
calls
720+
else
721+
describe_clauses =
722+
describe_clauses ++
607723
quote do
608-
context = unquote(acc)
609-
unquote(compile_merge(callback_describe))
724+
_ -> var!(context)
610725
end
611-
end)
726+
727+
quote do
728+
var!(context) = unquote(calls)
729+
case Map.get(var!(context), :describe, nil), do: unquote(describe_clauses)
730+
end
612731
end
613732

614733
quote do
615-
def __ex_unit__(unquote(kind), context) do
616-
describe = Map.get(context, :describe, nil)
617-
unquote(acc)
618-
end
734+
def __ex_unit__(unquote(kind), var!(context)), do: unquote(body)
619735
end
620736
end
621737

622-
defp compile_merge({callback, nil}) do
623-
quote do
624-
unquote(__MODULE__).__merge__(__MODULE__, context, unquote(callback)(context))
625-
end
738+
defp compile_setup([]) do
739+
quote(do: var!(context))
626740
end
627741

628-
defp compile_merge({callback, {_line, describe}}) do
629-
quote do
630-
if unquote(describe) == describe do
631-
unquote(compile_merge({callback, nil}))
632-
else
633-
context
742+
defp compile_setup(callbacks) do
743+
[h | t] = Enum.reverse(callbacks)
744+
745+
Enum.reduce(t, compile_setup_call(h), fn callback, acc ->
746+
quote do
747+
var!(context) = unquote(acc)
748+
unquote(compile_setup_call(callback))
634749
end
750+
end)
751+
end
752+
753+
defp compile_setup_call(callback) do
754+
quote do
755+
unquote(__MODULE__).__merge__(__MODULE__, var!(context), unquote(callback)(var!(context)))
635756
end
636757
end
637758
end

lib/ex_unit/lib/ex_unit/case.ex

Lines changed: 18 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -290,17 +290,15 @@ defmodule ExUnit.Case do
290290
:moduletag,
291291
:ex_unit_registered_test_attributes,
292292
:ex_unit_registered_describe_attributes,
293-
:ex_unit_registered_module_attributes,
294-
:ex_unit_used_describes
293+
:ex_unit_registered_module_attributes
295294
]
296295

297296
Enum.each(attributes, &Module.register_attribute(module, &1, accumulate: true))
298297

299298
attributes = [
300299
before_compile: ExUnit.Case,
301300
after_compile: ExUnit.Case,
302-
ex_unit_async: false,
303-
ex_unit_describe: nil
301+
ex_unit_async: false
304302
]
305303

306304
Enum.each(attributes, fn {k, v} -> Module.put_attribute(module, k, v) end)
@@ -454,48 +452,23 @@ defmodule ExUnit.Case do
454452
setup steps involved.
455453
"""
456454
defmacro describe(message, do: block) do
457-
quote do
458-
ExUnit.Case.__describe__(__MODULE__, __ENV__.line, unquote(message), fn ->
459-
unquote(block)
460-
end)
461-
end
462-
end
463-
464-
@doc false
465-
def __describe__(module, line, message, fun) do
466-
if Module.get_attribute(module, :ex_unit_describe) do
467-
raise "cannot call \"describe\" inside another \"describe\". See the documentation " <>
468-
"for ExUnit.Case.describe/2 on named setups and how to handle hierarchies"
469-
end
470-
471-
cond do
472-
not is_binary(message) ->
473-
raise ArgumentError, "describe name must be a string, got: #{inspect(message)}"
474-
475-
message in Module.get_attribute(module, :ex_unit_used_describes) ->
476-
raise ExUnit.DuplicateDescribeError,
477-
"describe #{inspect(message)} is already defined in #{inspect(module)}"
478-
479-
true ->
480-
:ok
481-
end
482-
483-
if Module.get_attribute(module, :describetag) != [] do
484-
raise "@describetag must be set inside describe/2 blocks"
485-
end
455+
definition =
456+
quote unquote: false do
457+
defp unquote(name)(var!(context)), do: unquote(body)
458+
end
486459

487-
Module.put_attribute(module, :ex_unit_describe, {line, message})
488-
Module.put_attribute(module, :ex_unit_used_describes, message)
460+
quote do
461+
ExUnit.Callbacks.__describe__(__MODULE__, __ENV__.line, unquote(message), fn
462+
message, describes ->
463+
res = unquote(block)
489464

490-
try do
491-
fun.()
492-
after
493-
Module.put_attribute(module, :ex_unit_describe, nil)
494-
Module.delete_attribute(module, :describetag)
465+
case ExUnit.Callbacks.__describe__(__MODULE__, message, describes) do
466+
{nil, nil} -> :ok
467+
{name, body} -> unquote(definition)
468+
end
495469

496-
for attribute <- Module.get_attribute(module, :ex_unit_registered_describe_attributes) do
497-
Module.delete_attribute(module, attribute)
498-
end
470+
res
471+
end)
499472
end
500473
end
501474

@@ -554,11 +527,11 @@ defmodule ExUnit.Case do
554527

555528
{name, describe, describe_line, describetag} =
556529
case Module.get_attribute(mod, :ex_unit_describe) do
557-
{line, describe} ->
530+
{line, describe, _counter} ->
558531
description = :"#{test_type} #{describe} #{name}"
559532
{description, describe, line, Module.get_attribute(mod, :describetag)}
560533

561-
_ ->
534+
nil ->
562535
{:"#{test_type} #{name}", nil, nil, []}
563536
end
564537

lib/ex_unit/test/ex_unit/describe_test.exs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,23 @@ defmodule ExUnit.DescribeTest do
2727
[setup_tag: :from_describe]
2828
end
2929

30-
test "from describe has higher precedence", context do
30+
test "from describe runs later", context do
3131
assert context.setup_tag == :from_describe
3232
end
3333
end
3434

35+
describe "setup from import" do
36+
import Map
37+
setup :to_list
38+
39+
test "is expanded within describe block", context do
40+
assert context.setup_tag == :from_module
41+
end
42+
end
43+
3544
describe "failures" do
3645
test "when using setup_all inside describe" do
37-
assert_raise RuntimeError, ~r"cannot invoke setup_all/2 inside describe", fn ->
46+
assert_raise RuntimeError, ~r"cannot invoke setup_all/1-2 inside describe", fn ->
3847
defmodule Sample do
3948
use ExUnit.Case
4049

0 commit comments

Comments
 (0)