Skip to content

Commit 44a1404

Browse files
author
José Valim
committed
Merge pull request #1059 from mururu/io_protocol
Implement capture_io for ex_unit
2 parents 1250c70 + 66eb369 commit 44a1404

File tree

2 files changed

+331
-0
lines changed

2 files changed

+331
-0
lines changed

lib/ex_unit/lib/ex_unit/capture_io.ex

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
defmodule ExUnit.CaptureIO do
2+
@moduledoc """
3+
This module provides functionality to capture IO to test it.
4+
The way to use this module is to import them into your module.
5+
6+
## Examples
7+
8+
defmodule AssertionTest do
9+
use ExUnit.Case
10+
11+
import ExUnit.CaptureIO
12+
13+
test :example do
14+
assert capture_io(fn ->
15+
IO.puts "a"
16+
end) == "a\n"
17+
end
18+
end
19+
20+
"""
21+
22+
@doc """
23+
Captures IO. Returns nil in case of no output.
24+
Otherwise returns the binary which is captured outputs.
25+
The input is mocked to return :eof.
26+
27+
## Examples
28+
29+
iex> capture_io(fn -> IO.write "josé" end) == "josé"
30+
true
31+
iex> capture_io(fn -> :ok end) == nil
32+
true
33+
34+
"""
35+
def capture_io(fun) do
36+
original_gl = :erlang.group_leader
37+
capture_gl = new_group_leader(self)
38+
39+
:erlang.group_leader(capture_gl, self)
40+
fun.()
41+
:erlang.group_leader(original_gl, self)
42+
43+
group_leader_sync(capture_gl)
44+
end
45+
46+
defp new_group_leader(runner) do
47+
spawn_link(fn -> group_leader_process(runner) end)
48+
end
49+
50+
defp group_leader_process(runner) do
51+
group_leader_loop(runner, :infinity, [])
52+
end
53+
54+
defp group_leader_loop(runner, wait, buf) do
55+
receive do
56+
{ :io_request, from, reply_as, req } ->
57+
p = :erlang.process_flag(:priority, :normal)
58+
buf = io_request(from, reply_as, req, buf)
59+
:erlang.process_flag(:priority, p)
60+
group_leader_loop(runner, wait, buf)
61+
:stop ->
62+
receive after: (2 -> :ok)
63+
:erlang.process_flag(:priority, :low)
64+
group_leader_loop(runner, 0, buf)
65+
_ ->
66+
group_leader_loop(runner, 0, buf)
67+
after wait ->
68+
:erlang.process_flag(:priority, :normal)
69+
runner <- { self, buffer_to_result(buf) }
70+
end
71+
end
72+
73+
defp group_leader_sync(gl) do
74+
gl <- :stop
75+
76+
receive do
77+
{ ^gl, buf } -> buf
78+
end
79+
end
80+
81+
defp io_request(from, reply_as, req, buf) do
82+
{ reply, buf1 } = io_request(req, buf)
83+
io_reply(from, reply_as, reply)
84+
buf1
85+
end
86+
87+
defp io_reply(from, reply_as, reply) do
88+
from <- { :io_reply, reply_as, reply }
89+
end
90+
91+
defp io_request({ :put_chars, chars }, buf) do
92+
{ :ok, [chars|buf] }
93+
end
94+
95+
defp io_request({ :put_chars, m, f, as }, buf) do
96+
chars = apply(m ,f, as)
97+
{ :ok, [chars|buf] }
98+
end
99+
100+
defp io_request({ :put_chars, _enc, chars }, buf) do
101+
io_request({ :put_chars, chars }, buf)
102+
end
103+
104+
defp io_request({ :put_chars, _enc, mod, func, args }, buf) do
105+
io_request({ :put_chars, mod, func, args }, buf)
106+
end
107+
108+
defp io_request({ :get_chars, _enc, _propmpt, _n }, buf) do
109+
{ :eof, buf }
110+
end
111+
112+
defp io_request({ :get_chars, _prompt, _n }, buf) do
113+
{ :eof, buf }
114+
end
115+
116+
defp io_request({ :get_line, _prompt }, buf) do
117+
{ :eof, buf }
118+
end
119+
120+
defp io_request({ :get_line, _enc, _prompt }, buf) do
121+
{ :eof, buf }
122+
end
123+
124+
defp io_request({ :get_until, _prompt, _m, _f, _as }, buf) do
125+
{ :eof, buf }
126+
end
127+
128+
defp io_request({ :setopts, _opts }, buf) do
129+
{ :ok, buf }
130+
end
131+
132+
defp io_request(:getopts, buf) do
133+
{ { :error, :enotsup }, buf }
134+
end
135+
136+
defp io_request({ :get_geometry, :columns }, buf) do
137+
{ { :error, :enotsup }, buf }
138+
end
139+
140+
defp io_request({ :get_geometry, :rows }, buf) do
141+
{ { :error, :enotsup }, buf }
142+
end
143+
144+
defp io_request({ :requests, reqs }, buf) do
145+
io_requests(reqs, { :ok, buf })
146+
end
147+
148+
defp io_request(_, buf) do
149+
{ { :error, :request }, buf }
150+
end
151+
152+
defp io_requests([r|rs], { :ok, buf }) do
153+
io_requests(rs, io_request(r, buf))
154+
end
155+
156+
defp io_requests(_, result) do
157+
result
158+
end
159+
160+
defp buffer_to_result([]) do
161+
nil
162+
end
163+
164+
defp buffer_to_result([bin]) when is_binary(bin) do
165+
bin
166+
end
167+
168+
defp buffer_to_result(buf) do
169+
buf |> :lists.reverse |> list_to_binary
170+
end
171+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
Code.require_file "../test_helper.exs", __DIR__
2+
3+
defmodule ExUnit.CaptureIOTest.Value do
4+
def binary, do: "a"
5+
end
6+
7+
alias ExUnit.CaptureIOTest.Value
8+
9+
defmodule ExUnit.CaptureIOTest do
10+
use ExUnit.Case, async: true
11+
12+
doctest ExUnit.CaptureIO, import: true
13+
14+
import ExUnit.CaptureIO
15+
16+
test :capture_io_with_nothing do
17+
assert capture_io(fn ->
18+
end) == nil
19+
end
20+
21+
test :capture_io_with_put_chars do
22+
assert capture_io(fn ->
23+
:io.put_chars("")
24+
end) == ""
25+
26+
assert capture_io(fn ->
27+
:io.put_chars("a")
28+
:io.put_chars("b")
29+
end) == "ab"
30+
31+
assert capture_io(fn ->
32+
send_and_receive_io({ :put_chars, :unicode, Value, :binary, [] })
33+
end) == "a"
34+
35+
assert capture_io(fn ->
36+
:io.put_chars("josé")
37+
end) == "josé"
38+
39+
assert capture_io(fn ->
40+
assert :io.put_chars("a") == :ok
41+
end)
42+
end
43+
44+
test :capture_io_with_put_chars_to_stderr do
45+
assert capture_io(fn ->
46+
:io.put_chars(:standard_error, "a")
47+
end) == nil
48+
end
49+
50+
test :capture_io_with_get_chars do
51+
assert capture_io(fn ->
52+
:io.get_chars(">", 3)
53+
end) == nil
54+
55+
capture_io(fn ->
56+
assert :io.get_chars(">", 3) == :eof
57+
end)
58+
end
59+
60+
test :capture_io_with_get_line do
61+
assert capture_io(fn ->
62+
:io.get_line ">"
63+
end) == nil
64+
65+
capture_io(fn ->
66+
assert :io.get_line(">") == :eof
67+
end)
68+
end
69+
70+
test :capture_io_with_get_until do
71+
assert capture_io(fn ->
72+
send_and_receive_io({ :get_until, '>', :m, :f, :as })
73+
end) == nil
74+
75+
capture_io(fn ->
76+
assert send_and_receive_io({ :get_until, '>', :m, :f, :as }) == :eof
77+
end)
78+
end
79+
80+
test :capture_io_with_setopts do
81+
assert capture_io(fn ->
82+
:io.setopts({ :encoding, :latin1 })
83+
end) == nil
84+
85+
capture_io(fn ->
86+
assert :io.setopts({ :encoding, :latin1 }) == :ok
87+
end)
88+
end
89+
90+
test :capture_io_with_getopts do
91+
assert capture_io(fn ->
92+
:io.getopts
93+
end) == nil
94+
95+
capture_io(fn ->
96+
assert :io.getopts == { :error, :enotsup }
97+
end)
98+
end
99+
100+
test :capture_io_with_columns do
101+
assert capture_io(fn ->
102+
:io.columns
103+
end) == nil
104+
105+
capture_io(fn ->
106+
assert :io.columns == { :error, :enotsup }
107+
end)
108+
end
109+
110+
test :capture_io_with_rows do
111+
assert capture_io(fn ->
112+
:io.rows
113+
end) == nil
114+
115+
capture_io(fn ->
116+
assert :io.rows == { :error, :enotsup }
117+
end)
118+
end
119+
120+
test :capture_io_with_multiple_io_requests do
121+
assert capture_io(fn ->
122+
send_and_receive_io({ :requests, [{ :put_chars, :unicode, "a" },
123+
{ :put_chars, :unicode, "b" }]})
124+
end) == "ab"
125+
126+
capture_io(fn ->
127+
assert send_and_receive_io({ :requests, [{ :put_chars, :unicode, "a" },
128+
{ :put_chars, :unicode, "b" }]}) == :ok
129+
end)
130+
end
131+
132+
test :caputure_io_with_unknown_io_request do
133+
assert capture_io(fn ->
134+
send_and_receive_io(:unknown)
135+
end) == nil
136+
137+
capture_io(fn ->
138+
assert send_and_receive_io(:unknown) == { :error, :request }
139+
end)
140+
end
141+
142+
test :capture_io_with_inside_assert do
143+
try do
144+
capture_io(fn ->
145+
assert false
146+
end)
147+
rescue
148+
error in [ExUnit.AssertionError] ->
149+
"Expected false to be true" = error.message
150+
end
151+
end
152+
153+
defp send_and_receive_io(req) do
154+
:erlang.group_leader <- { :io_request, self, self, req }
155+
s = self
156+
receive do
157+
{ :io_reply, ^s, res} -> res
158+
end
159+
end
160+
end

0 commit comments

Comments
 (0)