diff --git a/lib/ex_unit/lib/ex_unit/capture_io.ex b/lib/ex_unit/lib/ex_unit/capture_io.ex new file mode 100644 index 00000000000..c6893928722 --- /dev/null +++ b/lib/ex_unit/lib/ex_unit/capture_io.ex @@ -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 diff --git a/lib/ex_unit/test/ex_unit/capture_io_test.exs b/lib/ex_unit/test/ex_unit/capture_io_test.exs new file mode 100644 index 00000000000..e799aad219f --- /dev/null +++ b/lib/ex_unit/test/ex_unit/capture_io_test.exs @@ -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