Skip to content

Commit

Permalink
Merge pull request #1059 from mururu/io_protocol
Browse files Browse the repository at this point in the history
Implement capture_io for ex_unit
  • Loading branch information
José Valim committed May 15, 2013
2 parents 1250c70 + 66eb369 commit 44a1404
Show file tree
Hide file tree
Showing 2 changed files with 331 additions and 0 deletions.
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
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)
: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

0 comments on commit 44a1404

Please sign in to comment.