Skip to content
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

Merged
merged 1 commit into from
May 15, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions lib/ex_unit/lib/ex_unit/capture_io.ex
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
Copy link
Contributor

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?

Copy link
Contributor Author

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?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason for 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this line is not need. I'll remove it.

Copy link
Member

Choose a reason for hiding this comment

The 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
160 changes: 160 additions & 0 deletions lib/ex_unit/test/ex_unit/capture_io_test.exs
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