Skip to content

Commit 2d4e2b0

Browse files
committed
ex_unit: Add :capture_io tag
1 parent efcd164 commit 2d4e2b0

File tree

5 files changed

+125
-7
lines changed

5 files changed

+125
-7
lines changed

lib/ex_unit/lib/ex_unit.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,21 @@ defmodule ExUnit do
149149
* `:time` - the duration in microseconds of the test's runtime
150150
* `:tags` - the test tags
151151
* `:logs` - the captured logs
152+
* `:capture_io` - (since v1.20.0) the captured IO
152153
* `:parameters` - the test parameters
153154
154155
"""
155-
defstruct [:name, :case, :module, :state, time: 0, tags: %{}, logs: "", parameters: %{}]
156+
defstruct [
157+
:name,
158+
:case,
159+
:module,
160+
:state,
161+
time: 0,
162+
tags: %{},
163+
logs: "",
164+
capture_io: "",
165+
parameters: %{}
166+
]
156167

157168
# TODO: Remove the `:case` field on v2.0
158169
@type t :: %__MODULE__{

lib/ex_unit/lib/ex_unit/case.ex

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ defmodule ExUnit.Case do
183183
184184
The following tags customize how tests behave:
185185
186+
* `:capture_io` - (since v1.20.0) see the "IO Capture" section below
187+
186188
* `:capture_log` - see the "Log Capture" section below
187189
188190
* `:skip` - skips the test with the given reason
@@ -258,6 +260,34 @@ defmodule ExUnit.Case do
258260
Keep in mind that all tests are included by default, so unless they are
259261
excluded first, the `include` option has no effect.
260262
263+
## IO Capture
264+
265+
ExUnit can optionally suppress printing of standard output messages generated
266+
during a test. Messages generated while running a test are captured and
267+
only if the test fails are they printed to aid with debugging.
268+
269+
The captured IO is available in the test context under `:capture_io`
270+
key and can be read using `StringIO.flush/1`:
271+
272+
defmodule MyTest do
273+
use ExUnit.Case, async: true
274+
275+
@tag :capture_io
276+
test "with io", %{capture_io: io} do
277+
IO.puts("Hello, World!")
278+
279+
assert StringIO.flush(io) == "Hello, World!\n"
280+
end
281+
end
282+
283+
As with other tags, `:capture_io` can also be set as `@moduletag` and
284+
`@describetag`.
285+
286+
Since `setup_all` blocks don't belong to a specific test, standard output
287+
messages generated in them (or between tests) are never captured.
288+
289+
See also `ExUnit.CaptureIO`.
290+
261291
## Log Capture
262292
263293
ExUnit can optionally suppress printing of log messages that are generated
@@ -278,6 +308,8 @@ defmodule ExUnit.Case do
278308
279309
config :logger, :default_handler, false
280310
311+
See also `ExUnit.CaptureLog`.
312+
281313
## Tmp Dir
282314
283315
ExUnit automatically creates a temporary directory for tests tagged with

lib/ex_unit/lib/ex_unit/cli_formatter.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ defmodule ExUnit.CLIFormatter do
132132
)
133133

134134
print_failure(formatted, config)
135+
print_capture_io(test.capture_io)
135136
print_logs(test.logs)
136137

137138
test_counter = update_test_counter(config.test_counter, test)
@@ -519,4 +520,12 @@ defmodule ExUnit.CLIFormatter do
519520
output = String.replace(output, "\n", indent)
520521
IO.puts([" The following output was logged:", indent | output])
521522
end
523+
524+
defp print_capture_io(""), do: nil
525+
526+
defp print_capture_io(output) do
527+
indent = "\n "
528+
output = String.replace(output, "\n", indent)
529+
IO.puts([" The following standard output was captured:", indent | output])
530+
end
522531
end

lib/ex_unit/lib/ex_unit/runner.ex

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -439,16 +439,19 @@ defmodule ExUnit.Runner do
439439
generate_test_seed(seed, test, rand_algorithm)
440440
context = context |> Map.merge(test.tags) |> Map.put(:test_pid, self())
441441
capture_log = Map.get(context, :capture_log, capture_log)
442+
capture_io = Map.get(context, :capture_io, false)
442443

443444
{time, test} =
444445
:timer.tc(
445446
maybe_capture_log(capture_log, test, fn ->
446-
context = maybe_create_tmp_dir(context, test)
447-
448-
case exec_test_setup(test, context) do
449-
{:ok, context} -> exec_test(test, context)
450-
{:error, test} -> test
451-
end
447+
maybe_capture_io(capture_io, context, fn context ->
448+
context = maybe_create_tmp_dir(context, test)
449+
450+
case exec_test_setup(test, context) do
451+
{:ok, context} -> exec_test(test, context)
452+
{:error, test} -> test
453+
end
454+
end)
452455
end)
453456
)
454457

@@ -482,6 +485,32 @@ defmodule ExUnit.Runner do
482485
end
483486
end
484487

488+
defp maybe_capture_io(true, context, fun) do
489+
{:ok, gl} = StringIO.open("")
490+
Process.group_leader(self(), gl)
491+
context = put_in(context.capture_io, gl)
492+
test = fun.(context)
493+
put_in(test.capture_io, StringIO.flush(gl))
494+
end
495+
496+
defp maybe_capture_io(false, context, fun) do
497+
fun.(context)
498+
end
499+
500+
defp maybe_capture_io(other, _context, _fun) do
501+
raise ArgumentError, """
502+
invalid value for @tag :capture_io, expected one of:
503+
504+
@tag :capture_io
505+
@tag capture_io: true
506+
@tag capture_io: false
507+
508+
got:
509+
510+
@tag capture_io: #{inspect(other)}
511+
"""
512+
end
513+
485514
defp receive_test_reply(test, test_pid, test_ref, timeout) do
486515
receive do
487516
{^test_pid, :test_finished, test} ->

lib/ex_unit/test/ex_unit_test.exs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,43 @@ defmodule ExUnitTest do
350350
assert output =~ "\n1 test, 1 failure (3 excluded)\n"
351351
end
352352

353+
test "io capturing" do
354+
defmodule IOCapturingTest do
355+
use ExUnit.Case
356+
357+
@tag :capture_io
358+
test "one" do
359+
# test successful, captured "one" isn't printed
360+
IO.puts("one")
361+
assert 1 == 1
362+
end
363+
364+
@tag :capture_io
365+
test "two" do
366+
# test failed, captured "two" is printed
367+
IO.puts("two")
368+
assert 1 == 2
369+
end
370+
371+
@tag :capture_io
372+
test "three, four", %{capture_io: io} do
373+
# io is flushed, captured "three" isn't printed
374+
IO.puts("three")
375+
assert StringIO.flush(io) == "three\n"
376+
377+
# test failed, captured "four" is printed
378+
IO.puts("four")
379+
assert 1 == 2
380+
end
381+
end
382+
383+
output = capture_io(&ExUnit.run/0)
384+
refute output =~ "one\n"
385+
assert output =~ "two\n"
386+
refute output =~ "three\n"
387+
assert output =~ "four\n"
388+
end
389+
353390
test "log capturing" do
354391
defmodule LogCapturingTest do
355392
use ExUnit.Case

0 commit comments

Comments
 (0)