-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement capture_io for ex_unit #1059
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
defmodule ExUnit.CaptureIO do | ||
@moduledoc """ | ||
This module provides functionality to capture IO to test it. | ||
The way to use this module is to import them into your module. | ||
|
||
## Examples | ||
|
||
defmodule AssertionTest do | ||
use ExUnit.Case | ||
|
||
import ExUnit.CaptureIO | ||
|
||
test :example do | ||
assert capture_io(fn -> | ||
IO.puts "a" | ||
end) == "a\n" | ||
end | ||
end | ||
|
||
""" | ||
|
||
@doc """ | ||
Captures IO. Returns nil in case of no output. | ||
Otherwise returns the binary which is captured outputs. | ||
The input is mocked to return :eof. | ||
|
||
## Examples | ||
|
||
iex> capture_io(fn -> IO.write "josé" end) == "josé" | ||
true | ||
iex> capture_io(fn -> :ok end) == nil | ||
true | ||
|
||
""" | ||
def capture_io(fun) do | ||
original_gl = :erlang.group_leader | ||
capture_gl = new_group_leader(self) | ||
|
||
:erlang.group_leader(capture_gl, self) | ||
fun.() | ||
:erlang.group_leader(original_gl, self) | ||
|
||
group_leader_sync(capture_gl) | ||
end | ||
|
||
defp new_group_leader(runner) do | ||
spawn_link(fn -> group_leader_process(runner) end) | ||
end | ||
|
||
defp group_leader_process(runner) do | ||
group_leader_loop(runner, :infinity, []) | ||
end | ||
|
||
defp group_leader_loop(runner, wait, buf) do | ||
receive do | ||
{ :io_request, from, reply_as, req } -> | ||
p = :erlang.process_flag(:priority, :normal) | ||
buf = io_request(from, reply_as, req, buf) | ||
:erlang.process_flag(:priority, p) | ||
group_leader_loop(runner, wait, buf) | ||
:stop -> | ||
receive after: (2 -> :ok) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the reason for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, this line is not need. I'll remove it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For what is worth, I think none of the io implementations in otp use a gen server (not the file ones and not the one in eunit) |
||
:erlang.process_flag(:priority, :low) | ||
group_leader_loop(runner, 0, buf) | ||
_ -> | ||
group_leader_loop(runner, 0, buf) | ||
after wait -> | ||
:erlang.process_flag(:priority, :normal) | ||
runner <- { self, buffer_to_result(buf) } | ||
end | ||
end | ||
|
||
defp group_leader_sync(gl) do | ||
gl <- :stop | ||
|
||
receive do | ||
{ ^gl, buf } -> buf | ||
end | ||
end | ||
|
||
defp io_request(from, reply_as, req, buf) do | ||
{ reply, buf1 } = io_request(req, buf) | ||
io_reply(from, reply_as, reply) | ||
buf1 | ||
end | ||
|
||
defp io_reply(from, reply_as, reply) do | ||
from <- { :io_reply, reply_as, reply } | ||
end | ||
|
||
defp io_request({ :put_chars, chars }, buf) do | ||
{ :ok, [chars|buf] } | ||
end | ||
|
||
defp io_request({ :put_chars, m, f, as }, buf) do | ||
chars = apply(m ,f, as) | ||
{ :ok, [chars|buf] } | ||
end | ||
|
||
defp io_request({ :put_chars, _enc, chars }, buf) do | ||
io_request({ :put_chars, chars }, buf) | ||
end | ||
|
||
defp io_request({ :put_chars, _enc, mod, func, args }, buf) do | ||
io_request({ :put_chars, mod, func, args }, buf) | ||
end | ||
|
||
defp io_request({ :get_chars, _enc, _propmpt, _n }, buf) do | ||
{ :eof, buf } | ||
end | ||
|
||
defp io_request({ :get_chars, _prompt, _n }, buf) do | ||
{ :eof, buf } | ||
end | ||
|
||
defp io_request({ :get_line, _prompt }, buf) do | ||
{ :eof, buf } | ||
end | ||
|
||
defp io_request({ :get_line, _enc, _prompt }, buf) do | ||
{ :eof, buf } | ||
end | ||
|
||
defp io_request({ :get_until, _prompt, _m, _f, _as }, buf) do | ||
{ :eof, buf } | ||
end | ||
|
||
defp io_request({ :setopts, _opts }, buf) do | ||
{ :ok, buf } | ||
end | ||
|
||
defp io_request(:getopts, buf) do | ||
{ { :error, :enotsup }, buf } | ||
end | ||
|
||
defp io_request({ :get_geometry, :columns }, buf) do | ||
{ { :error, :enotsup }, buf } | ||
end | ||
|
||
defp io_request({ :get_geometry, :rows }, buf) do | ||
{ { :error, :enotsup }, buf } | ||
end | ||
|
||
defp io_request({ :requests, reqs }, buf) do | ||
io_requests(reqs, { :ok, buf }) | ||
end | ||
|
||
defp io_request(_, buf) do | ||
{ { :error, :request }, buf } | ||
end | ||
|
||
defp io_requests([r|rs], { :ok, buf }) do | ||
io_requests(rs, io_request(r, buf)) | ||
end | ||
|
||
defp io_requests(_, result) do | ||
result | ||
end | ||
|
||
defp buffer_to_result([]) do | ||
nil | ||
end | ||
|
||
defp buffer_to_result([bin]) when is_binary(bin) do | ||
bin | ||
end | ||
|
||
defp buffer_to_result(buf) do | ||
buf |> :lists.reverse |> list_to_binary | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
Code.require_file "../test_helper.exs", __DIR__ | ||
|
||
defmodule ExUnit.CaptureIOTest.Value do | ||
def binary, do: "a" | ||
end | ||
|
||
alias ExUnit.CaptureIOTest.Value | ||
|
||
defmodule ExUnit.CaptureIOTest do | ||
use ExUnit.Case, async: true | ||
|
||
doctest ExUnit.CaptureIO, import: true | ||
|
||
import ExUnit.CaptureIO | ||
|
||
test :capture_io_with_nothing do | ||
assert capture_io(fn -> | ||
end) == nil | ||
end | ||
|
||
test :capture_io_with_put_chars do | ||
assert capture_io(fn -> | ||
:io.put_chars("") | ||
end) == "" | ||
|
||
assert capture_io(fn -> | ||
:io.put_chars("a") | ||
:io.put_chars("b") | ||
end) == "ab" | ||
|
||
assert capture_io(fn -> | ||
send_and_receive_io({ :put_chars, :unicode, Value, :binary, [] }) | ||
end) == "a" | ||
|
||
assert capture_io(fn -> | ||
:io.put_chars("josé") | ||
end) == "josé" | ||
|
||
assert capture_io(fn -> | ||
assert :io.put_chars("a") == :ok | ||
end) | ||
end | ||
|
||
test :capture_io_with_put_chars_to_stderr do | ||
assert capture_io(fn -> | ||
:io.put_chars(:standard_error, "a") | ||
end) == nil | ||
end | ||
|
||
test :capture_io_with_get_chars do | ||
assert capture_io(fn -> | ||
:io.get_chars(">", 3) | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert :io.get_chars(">", 3) == :eof | ||
end) | ||
end | ||
|
||
test :capture_io_with_get_line do | ||
assert capture_io(fn -> | ||
:io.get_line ">" | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert :io.get_line(">") == :eof | ||
end) | ||
end | ||
|
||
test :capture_io_with_get_until do | ||
assert capture_io(fn -> | ||
send_and_receive_io({ :get_until, '>', :m, :f, :as }) | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert send_and_receive_io({ :get_until, '>', :m, :f, :as }) == :eof | ||
end) | ||
end | ||
|
||
test :capture_io_with_setopts do | ||
assert capture_io(fn -> | ||
:io.setopts({ :encoding, :latin1 }) | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert :io.setopts({ :encoding, :latin1 }) == :ok | ||
end) | ||
end | ||
|
||
test :capture_io_with_getopts do | ||
assert capture_io(fn -> | ||
:io.getopts | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert :io.getopts == { :error, :enotsup } | ||
end) | ||
end | ||
|
||
test :capture_io_with_columns do | ||
assert capture_io(fn -> | ||
:io.columns | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert :io.columns == { :error, :enotsup } | ||
end) | ||
end | ||
|
||
test :capture_io_with_rows do | ||
assert capture_io(fn -> | ||
:io.rows | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert :io.rows == { :error, :enotsup } | ||
end) | ||
end | ||
|
||
test :capture_io_with_multiple_io_requests do | ||
assert capture_io(fn -> | ||
send_and_receive_io({ :requests, [{ :put_chars, :unicode, "a" }, | ||
{ :put_chars, :unicode, "b" }]}) | ||
end) == "ab" | ||
|
||
capture_io(fn -> | ||
assert send_and_receive_io({ :requests, [{ :put_chars, :unicode, "a" }, | ||
{ :put_chars, :unicode, "b" }]}) == :ok | ||
end) | ||
end | ||
|
||
test :caputure_io_with_unknown_io_request do | ||
assert capture_io(fn -> | ||
send_and_receive_io(:unknown) | ||
end) == nil | ||
|
||
capture_io(fn -> | ||
assert send_and_receive_io(:unknown) == { :error, :request } | ||
end) | ||
end | ||
|
||
test :capture_io_with_inside_assert do | ||
try do | ||
capture_io(fn -> | ||
assert false | ||
end) | ||
rescue | ||
error in [ExUnit.AssertionError] -> | ||
"Expected false to be true" = error.message | ||
end | ||
end | ||
|
||
defp send_and_receive_io(req) do | ||
:erlang.group_leader <- { :io_request, self, self, req } | ||
s = self | ||
receive do | ||
{ :io_reply, ^s, res} -> res | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any specific reason for not making this a gen_server?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No specific reason. Is it better to use gen_server?