Use Appsignal.Plug.handle_error/4 in PlugTest

Currently, PlugTest calls Appsignal.Plug.handle_error/5, which doesn't
exist and throws an exception. Due to the nature of the test, the
exception is caught and the tests pass.

This patch calls the existing function, which should still produce an
undefined error (as handle_error/4 reraises the caught error).

defmodule PlugWithAppSignal do
  use Plug.Router
  use Appsignal.Plug
  use Plug.ErrorHandler

  plug(:match)
  plug(:dispatch)

  get "/" do
    send_resp(conn, 200, "Welcome")
  end

  get "/overwritten" do
    Appsignal.FakeTransaction.set_action("AppsignalPhoenixExample.PageController#overwritten")

    send_resp(conn, 200, "Welcome")
  end

  get "/exception" do
    raise("Exception!")
    send_resp(conn, 200, "Welcome")
  end

  get "/bad_request" do
    raise %Plug.BadRequestError{}
    send_resp(conn, 200, "Welcome")
  end

  get "/undef" do
    raise %Plug.Conn.WrapperError{
      kind: :error,
      reason: :undef,
      stack: [],
      conn: %{conn | params: %{"foo" => "bar"}}
    }

    send_resp(conn, 200, "Welcome")
  end

  get "/no_transaction" do
    new_private = Map.delete(conn.private, :appsignal_transaction)

    raise %Plug.Conn.WrapperError{
      kind: :error,
      reason: :undef,
      stack: [],
      conn: %{conn | private: new_private}
    }

    send_resp(conn, 200, "Welcome")
  end

  get "/timeout" do
    Task.async(fn -> :timer.sleep(10) end) |> Task.await(1)

    send_resp(conn, 200, "Welcome")
  end

  defp handle_errors(conn, error) do
    send_resp(conn, conn.status, inspect(error))
  end
end

defmodule ModuleWithCall do
  defmacro __using__(_) do
    quote do
      def call(%Plug.Conn{} = conn, _opts) do
        Plug.Conn.assign(conn, :called?, true)
      end

      defoverridable call: 2
    end
  end
end

defmodule OverridingAppSignalPlug do
  use ModuleWithCall
  use Appsignal.Plug

  def call(conn, opts) do
    conn = super(conn, opts)
    Plug.Conn.assign(conn, :overridden?, true)
  end
end

defmodule Appsignal.PlugTest do
  alias Appsignal.FakeTransaction
  import AppsignalTest.Utils
  use ExUnit.Case
  use Plug.Test

  setup do
    {:ok, fake_transaction} = FakeTransaction.start_link()
    [fake_transaction: fake_transaction]
  end

  describe "for a :sample transaction" do
    setup do
      conn =
        :get
        |> conn("/", "")
        |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
        |> Plug.Conn.put_private(:phoenix_action, :index)
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "starts a transaction", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.started_transaction?(fake_transaction)
    end

    test "returns the updated conn", %{conn: conn} do
      assert conn.state == :sent
    end

    test "adds the transaction to the conn", %{conn: conn} do
      assert %Appsignal.Transaction{id: "123"} = conn.private[:appsignal_transaction]
    end

    test "sets the transaction's action name", %{fake_transaction: fake_transaction} do
      assert "AppsignalPhoenixExample.PageController#index" ==
               FakeTransaction.action(fake_transaction)
    end

    test "finishes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.finished_transactions(fake_transaction)
    end

    test "sets the transaction's request metadata", %{
      conn: conn,
      fake_transaction: fake_transaction
    } do
      assert conn == FakeTransaction.request_metadata(fake_transaction)
    end

    test "completes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.completed_transactions(fake_transaction)
    end
  end

  describe "for a :no_sample transaction" do
    setup %{fake_transaction: fake_transaction} do
      FakeTransaction.update(fake_transaction, :finish, :no_sample)

      conn =
        :get
        |> conn("/", "")
        |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
        |> Plug.Conn.put_private(:phoenix_action, :index)
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "does not set the transaction's request metadata", %{fake_transaction: fake_transaction} do
      assert nil == FakeTransaction.request_metadata(fake_transaction)
    end
  end

  describe "for a transaction without a Phoenix endpoint" do
    setup do
      conn =
        :get
        |> conn("/", "")
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "does not set the transaction's action name", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.action(fake_transaction) == "unknown"
    end
  end

  describe "for a transaction with a Phoenix endpoint, but no action" do
    setup do
      conn =
        :get
        |> conn("/", "")
        |> Plug.Conn.put_private(:phoenix_endpoint, MyEndpoint)
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "does not set the transaction's action name", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.action(fake_transaction) == nil
    end
  end

  describe "for a transaction with an overwritten action name" do
    setup do
      conn =
        :get
        |> conn("/overwritten", "")
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "sets the transaction's action name", %{fake_transaction: fake_transaction} do
      assert "AppsignalPhoenixExample.PageController#overwritten" ==
               FakeTransaction.action(fake_transaction)
    end
  end

  describe "for a transaction with an error" do
    setup do
      try do
        :get
        |> conn("/exception", %{"foo" => "bar"})
        |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
        |> Plug.Conn.put_private(:phoenix_action, :exception)
        |> PlugWithAppSignal.call([])
      catch
        :error, %Plug.Conn.WrapperError{reason: %RuntimeError{message: "Exception!"}} ->
          :ok

        type, reason ->
          {type, reason}
      end
    end

    test "sets the transaction error", %{fake_transaction: fake_transaction} do
      assert [
               {
                 %Appsignal.Transaction{},
                 "RuntimeError",
                 "Exception!",
                 _stack
               }
             ] = FakeTransaction.errors(fake_transaction)
    end

    test "sets the transaction's action name", %{fake_transaction: fake_transaction} do
      assert "AppsignalPhoenixExample.PageController#exception" ==
               FakeTransaction.action(fake_transaction)
    end

    test "finishes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.finished_transactions(fake_transaction)
    end

    test "sets the transaction's request metadata", %{
      fake_transaction: fake_transaction
    } do
      assert %Plug.Conn{params: %{"foo" => "bar"}} =
               FakeTransaction.request_metadata(fake_transaction)
    end

    test "completes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.completed_transactions(fake_transaction)
    end

    test "ignores the process' pid" do
      until(fn ->
        assert Appsignal.TransactionRegistry.lookup(self()) == :ignored
      end)
    end
  end

  describe "for a transaction with a bad request error" do
    setup do
      [conn: conn(:get, "/bad_request", "")]
    end

    test "does not set the transaction error", %{conn: conn, fake_transaction: fake_transaction} do
      :ok =
        try do
          PlugWithAppSignal.call(conn, %{})
        catch
          :error, %Plug.Conn.WrapperError{reason: %Plug.BadRequestError{}} -> :ok
          type, reason -> {type, reason}
        end

      assert [] = FakeTransaction.errors(fake_transaction)
    end
  end

  describe "for a wrapped undefined error" do
    setup do
      :ok =
        try do
          :get
          |> conn("/undef", %{"foo" => "bar"})
          |> PlugWithAppSignal.call([])
        rescue
          Plug.Conn.WrapperError -> :ok
        end
    end

    test "sets the transaction error", %{fake_transaction: fake_transaction} do
      assert [
               {
                 %Appsignal.Transaction{},
                 "UndefinedFunctionError",
                 "undefined function",
                 _stack
               }
             ] = FakeTransaction.errors(fake_transaction)
    end

    test "sets the transaction's request metadata", %{
      fake_transaction: fake_transaction
    } do
      assert %Plug.Conn{params: %{"foo" => "bar"}} =
               FakeTransaction.request_metadata(fake_transaction)
    end
  end

  describe "for a conn without a transaction" do
    setup do
      :ok =
        try do
          :get
          |> conn("/no_transaction")
          |> PlugWithAppSignal.call([])
        rescue
          Plug.Conn.WrapperError -> :ok
        end
    end

    test "does not set a transaction error", %{fake_transaction: fake_transaction} do
      assert [] = FakeTransaction.errors(fake_transaction)
    end
  end

  describe "for a transaction with an timeout" do
    setup do
      conn = conn(:get, "/timeout")

      [conn: conn]
    end

    test "sets the transaction error", %{conn: conn, fake_transaction: fake_transaction} do
      :ok =
        try do
          PlugWithAppSignal.call(conn, [])
        catch
          :exit, {:timeout, {Task, :await, _}} -> :ok
          type, reason -> {type, reason}
        end

      [{%Appsignal.Transaction{}, name, message, _stack}] =
        FakeTransaction.errors(fake_transaction)

      assert name == ":timeout"
      assert message =~ ~r({:timeout, {Task, :await, \[%Task{owner: ...)
    end
  end

  describe "when AppSignal is disabled" do
    setup do
      conn =
        AppsignalTest.Utils.with_config(%{active: false}, fn ->
          :get
          |> conn("/", "")
          |> PlugWithAppSignal.call([])
        end)

      [conn: conn]
    end

    test "does not start a transaction", %{fake_transaction: fake_transaction} do
      refute FakeTransaction.started_transaction?(fake_transaction)
    end

    test "returns the updated conn", %{conn: conn} do
      assert conn.state == :sent
    end
  end

  describe "extracting action names" do
    test "from a Plug conn" do
      assert Appsignal.Plug.extract_action(%Plug.Conn{method: "GET", request_path: "/foo"}) ==
               "unknown"
    end

    test "from a Plug conn with a Phoenix endpoint, but no controller or action" do
      assert %Plug.Conn{}
             |> Plug.Conn.put_private(:phoenix_endpoint, MyEndpoint)
             |> Appsignal.Plug.extract_action() == nil
    end

    test "from a Plug conn with a Phoenix controller and action" do
      assert %Plug.Conn{}
             |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
             |> Plug.Conn.put_private(:phoenix_action, :index)
             |> Appsignal.Plug.extract_action() == "AppsignalPhoenixExample.PageController#index"
    end
  end

  describe "extracting request metadata" do
    test "from a Plug conn" do
      assert Appsignal.Plug.extract_meta_data(%Plug.Conn{
               method: "GET",
               request_path: "/foo",
               resp_headers: [{"x-request-id", "kk4hk5sis7c3b56t683nnmdig632c9ot"}],
               status: 200
             }) == %{
               "method" => "GET",
               "path" => "/foo",
               "request_id" => "kk4hk5sis7c3b56t683nnmdig632c9ot",
               "http_status_code" => 200
             }
    end
  end

  describe "extracting sample data" do
    setup do
      %{
        conn: %Plug.Conn{
          params: %{"foo" => "bar"},
          host: "www.example.com",
          method: "GET",
          script_name: ["foo", "bar"],
          request_path: "/foo/bar",
          port: 80,
          query_string: "foo=bar",
          scheme: :http,
          req_headers: [{"accept", "text/html"}]
        }
      }
    end

    test "from a Plug conn", %{conn: conn} do
      assert Appsignal.Plug.extract_sample_data(conn) == %{
               "params" => %{"foo" => "bar"},
               "environment" => %{
                 "host" => "www.example.com",
                 "method" => "GET",
                 "request_path" => "/foo/bar",
                 "port" => 80,
                 "request_uri" => "http://www.example.com:80/foo/bar",
                 "req_headers.accept" => "text/html"
               }
             }
    end

    test "with a param that should be filtered out", %{conn: conn} do
      AppsignalTest.Utils.with_config(%{filter_parameters: ["password"]}, fn ->
        conn = %{conn | params: %{"password" => "secret"}}

        assert %{"params" => %{"password" => "[FILTERED]"}} =
                 Appsignal.Plug.extract_sample_data(conn)
      end)
    end
  end

  describe "extracting request headers" do
    setup do
      [
        conn: %Plug.Conn{
          req_headers: [
            {"content-length", "1024"},
            {"accept", "text/html"},
            {"accept-charset", "utf-8"},
            {"accept-encoding", "gzip, deflate"},
            {"accept-language", "en-us"},
            {"cache-control", "no-cache"},
            {"connection", "keep-alive"},
            {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3..."},
            {"from", "webmaster@example.org"},
            {"referer", "http://localhost:4001/"},
            {"range", "bytes=0-1023"},
            {"cookie", "__ar_v4=U35IKTLTJNEP7GWW6OH3N2%3A20161120%3A90%7CI..."},
            {"x-real-ip", "179.146.231.170"}
          ]
        }
      ]
    end

    test "with header key configuration", %{conn: conn} do
      AppsignalTest.Utils.with_config(%{request_headers: ["content-length"]}, fn ->
        assert Appsignal.Plug.extract_request_headers(conn) == %{
                 "req_headers.content-length" => "1024"
               }
      end)
    end

    test "with fallback header keys", %{conn: conn} do
      assert Appsignal.Plug.extract_request_headers(conn) == %{
               "req_headers.content-length" => "1024",
               "req_headers.accept" => "text/html",
               "req_headers.accept-charset" => "utf-8",
               "req_headers.accept-encoding" => "gzip, deflate",
               "req_headers.accept-language" => "en-us",
               "req_headers.cache-control" => "no-cache",
               "req_headers.connection" => "keep-alive",
               "req_headers.range" => "bytes=0-1023"
             }
    end
  end

  describe "handling errors for a wrapped error" do
    setup do
      transaction = %Appsignal.Transaction{}
      conn = %Plug.Conn{private: %{appsignal_transaction: transaction}}
      kind = :error
      wrapped_reason = :undef
      stack = []

      reason = %Plug.Conn.WrapperError{
        kind: kind,
        reason: wrapped_reason,
        stack: stack,
        conn: conn
      }

      :ok =
        try do
          Appsignal.Plug.handle_error(conn, kind, reason, stack)
        rescue
          Plug.Conn.WrapperError -> :ok
        end

      [conn: conn]
    end

    test "sets the transaction error", %{fake_transaction: fake_transaction} do
      assert [
               {
                 %Appsignal.Transaction{},
                 "UndefinedFunctionError",
                 "undefined function",
                 []
               }
             ] == FakeTransaction.errors(fake_transaction)
    end

    test "sets the transaction's request metdata", %{
      fake_transaction: fake_transaction,
      conn: conn
    } do
      assert conn == FakeTransaction.request_metadata(fake_transaction)
    end
  end

  describe "handling errors for an error with a plug status < 500" do
    test "reraises the error" do
      :ok =
        try do
          transaction = %Appsignal.Transaction{}

          Appsignal.Plug.handle_error(
            %Plug.Conn{private: %{appsignal_transaction: transaction}},
            :error,
            %Plug.BadRequestError{},
            []
          )
        rescue
          Plug.BadRequestError -> :ok
        end
    end
  end

  describe "handling errors for a conn without a transaction" do
    test "reraises the error" do
      :ok =
        try do
          Appsignal.Plug.handle_error(%Plug.Conn{}, :error, :undef, [])
        rescue
          UndefinedFunctionError -> :ok
        end
    end
  end

  describe "when overriding the AppSignal Plug" do
    setup do
      conn = OverridingAppSignalPlug.call(%Plug.Conn{}, %{})

      [conn: conn]
    end

    test "starts a transaction", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.started_transaction?(fake_transaction)
    end

    test "calls super and returns the conn", %{conn: conn} do
      assert conn.assigns[:called?]
    end

    test "calls the overridden call/2", %{conn: conn} do
      assert conn.assigns[:overridden?]
    end
  end
end

defmodule PlugWithAppSignal do
  use Plug.Router
  use Appsignal.Plug
  use Plug.ErrorHandler

  plug(:match)
  plug(:dispatch)

  get "/" do
    send_resp(conn, 200, "Welcome")
  end

  get "/overwritten" do
    Appsignal.FakeTransaction.set_action("AppsignalPhoenixExample.PageController#overwritten")

    send_resp(conn, 200, "Welcome")
  end

  get "/exception" do
    raise("Exception!")
    send_resp(conn, 200, "Welcome")
  end

  get "/bad_request" do
    raise %Plug.BadRequestError{}
    send_resp(conn, 200, "Welcome")
  end

  get "/undef" do
    raise %Plug.Conn.WrapperError{
      kind: :error,
      reason: :undef,
      stack: [],
      conn: %{conn | params: %{"foo" => "bar"}}
    }

    send_resp(conn, 200, "Welcome")
  end

  get "/no_transaction" do
    new_private = Map.delete(conn.private, :appsignal_transaction)

    raise %Plug.Conn.WrapperError{
      kind: :error,
      reason: :undef,
      stack: [],
      conn: %{conn | private: new_private}
    }

    send_resp(conn, 200, "Welcome")
  end

  get "/timeout" do
    Task.async(fn -> :timer.sleep(10) end) |> Task.await(1)

    send_resp(conn, 200, "Welcome")
  end

  defp handle_errors(conn, error) do
    send_resp(conn, conn.status, inspect(error))
  end
end

defmodule ModuleWithCall do
  defmacro __using__(_) do
    quote do
      def call(%Plug.Conn{} = conn, _opts) do
        Plug.Conn.assign(conn, :called?, true)
      end

      defoverridable call: 2
    end
  end
end

defmodule OverridingAppSignalPlug do
  use ModuleWithCall
  use Appsignal.Plug

  def call(conn, opts) do
    conn = super(conn, opts)
    Plug.Conn.assign(conn, :overridden?, true)
  end
end

defmodule Appsignal.PlugTest do
  alias Appsignal.FakeTransaction
  import AppsignalTest.Utils
  use ExUnit.Case
  use Plug.Test

  setup do
    {:ok, fake_transaction} = FakeTransaction.start_link()
    [fake_transaction: fake_transaction]
  end

  describe "for a :sample transaction" do
    setup do
      conn =
        :get
        |> conn("/", "")
        |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
        |> Plug.Conn.put_private(:phoenix_action, :index)
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "starts a transaction", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.started_transaction?(fake_transaction)
    end

    test "returns the updated conn", %{conn: conn} do
      assert conn.state == :sent
    end

    test "adds the transaction to the conn", %{conn: conn} do
      assert %Appsignal.Transaction{id: "123"} = conn.private[:appsignal_transaction]
    end

    test "sets the transaction's action name", %{fake_transaction: fake_transaction} do
      assert "AppsignalPhoenixExample.PageController#index" ==
               FakeTransaction.action(fake_transaction)
    end

    test "finishes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.finished_transactions(fake_transaction)
    end

    test "sets the transaction's request metadata", %{
      conn: conn,
      fake_transaction: fake_transaction
    } do
      assert conn == FakeTransaction.request_metadata(fake_transaction)
    end

    test "completes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.completed_transactions(fake_transaction)
    end
  end

  describe "for a :no_sample transaction" do
    setup %{fake_transaction: fake_transaction} do
      FakeTransaction.update(fake_transaction, :finish, :no_sample)

      conn =
        :get
        |> conn("/", "")
        |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
        |> Plug.Conn.put_private(:phoenix_action, :index)
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "does not set the transaction's request metadata", %{fake_transaction: fake_transaction} do
      assert nil == FakeTransaction.request_metadata(fake_transaction)
    end
  end

  describe "for a transaction without a Phoenix endpoint" do
    setup do
      conn =
        :get
        |> conn("/", "")
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "does not set the transaction's action name", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.action(fake_transaction) == "unknown"
    end
  end

  describe "for a transaction with a Phoenix endpoint, but no action" do
    setup do
      conn =
        :get
        |> conn("/", "")
        |> Plug.Conn.put_private(:phoenix_endpoint, MyEndpoint)
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "does not set the transaction's action name", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.action(fake_transaction) == nil
    end
  end

  describe "for a transaction with an overwritten action name" do
    setup do
      conn =
        :get
        |> conn("/overwritten", "")
        |> PlugWithAppSignal.call([])

      [conn: conn]
    end

    test "sets the transaction's action name", %{fake_transaction: fake_transaction} do
      assert "AppsignalPhoenixExample.PageController#overwritten" ==
               FakeTransaction.action(fake_transaction)
    end
  end

  describe "for a transaction with an error" do
    setup do
      try do
        :get
        |> conn("/exception", %{"foo" => "bar"})
        |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
        |> Plug.Conn.put_private(:phoenix_action, :exception)
        |> PlugWithAppSignal.call([])
      catch
        :error, %Plug.Conn.WrapperError{reason: %RuntimeError{message: "Exception!"}} ->
          :ok

        type, reason ->
          {type, reason}
      end
    end

    test "sets the transaction error", %{fake_transaction: fake_transaction} do
      assert [
               {
                 %Appsignal.Transaction{},
                 "RuntimeError",
                 "Exception!",
                 _stack
               }
             ] = FakeTransaction.errors(fake_transaction)
    end

    test "sets the transaction's action name", %{fake_transaction: fake_transaction} do
      assert "AppsignalPhoenixExample.PageController#exception" ==
               FakeTransaction.action(fake_transaction)
    end

    test "finishes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.finished_transactions(fake_transaction)
    end

    test "sets the transaction's request metadata", %{
      fake_transaction: fake_transaction
    } do
      assert %Plug.Conn{params: %{"foo" => "bar"}} =
               FakeTransaction.request_metadata(fake_transaction)
    end

    test "completes the transaction", %{fake_transaction: fake_transaction} do
      assert [%Appsignal.Transaction{}] = FakeTransaction.completed_transactions(fake_transaction)
    end

    test "ignores the process' pid" do
      until(fn ->
        assert Appsignal.TransactionRegistry.lookup(self()) == :ignored
      end)
    end
  end

  describe "for a transaction with a bad request error" do
    setup do
      [conn: conn(:get, "/bad_request", "")]
    end

    test "does not set the transaction error", %{conn: conn, fake_transaction: fake_transaction} do
      :ok =
        try do
          PlugWithAppSignal.call(conn, %{})
        catch
          :error, %Plug.Conn.WrapperError{reason: %Plug.BadRequestError{}} -> :ok
          type, reason -> {type, reason}
        end

      assert [] = FakeTransaction.errors(fake_transaction)
    end
  end

  describe "for a wrapped undefined error" do
    setup do
      :ok =
        try do
          :get
          |> conn("/undef", %{"foo" => "bar"})
          |> PlugWithAppSignal.call([])
        rescue
          Plug.Conn.WrapperError -> :ok
        end
    end

    test "sets the transaction error", %{fake_transaction: fake_transaction} do
      assert [
               {
                 %Appsignal.Transaction{},
                 "UndefinedFunctionError",
                 "undefined function",
                 _stack
               }
             ] = FakeTransaction.errors(fake_transaction)
    end

    test "sets the transaction's request metadata", %{
      fake_transaction: fake_transaction
    } do
      assert %Plug.Conn{params: %{"foo" => "bar"}} =
               FakeTransaction.request_metadata(fake_transaction)
    end
  end

  describe "for a conn without a transaction" do
    setup do
      :ok =
        try do
          :get
          |> conn("/no_transaction")
          |> PlugWithAppSignal.call([])
        rescue
          Plug.Conn.WrapperError -> :ok
        end
    end

    test "does not set a transaction error", %{fake_transaction: fake_transaction} do
      assert [] = FakeTransaction.errors(fake_transaction)
    end
  end

  describe "for a transaction with an timeout" do
    setup do
      conn = conn(:get, "/timeout")

      [conn: conn]
    end

    test "sets the transaction error", %{conn: conn, fake_transaction: fake_transaction} do
      :ok =
        try do
          PlugWithAppSignal.call(conn, [])
        catch
          :exit, {:timeout, {Task, :await, _}} -> :ok
          type, reason -> {type, reason}
        end

      [{%Appsignal.Transaction{}, name, message, _stack}] =
        FakeTransaction.errors(fake_transaction)

      assert name == ":timeout"
      assert message =~ ~r({:timeout, {Task, :await, \[%Task{owner: ...)
    end
  end

  describe "when AppSignal is disabled" do
    setup do
      conn =
        AppsignalTest.Utils.with_config(%{active: false}, fn ->
          :get
          |> conn("/", "")
          |> PlugWithAppSignal.call([])
        end)

      [conn: conn]
    end

    test "does not start a transaction", %{fake_transaction: fake_transaction} do
      refute FakeTransaction.started_transaction?(fake_transaction)
    end

    test "returns the updated conn", %{conn: conn} do
      assert conn.state == :sent
    end
  end

  describe "extracting action names" do
    test "from a Plug conn" do
      assert Appsignal.Plug.extract_action(%Plug.Conn{method: "GET", request_path: "/foo"}) ==
               "unknown"
    end

    test "from a Plug conn with a Phoenix endpoint, but no controller or action" do
      assert %Plug.Conn{}
             |> Plug.Conn.put_private(:phoenix_endpoint, MyEndpoint)
             |> Appsignal.Plug.extract_action() == nil
    end

    test "from a Plug conn with a Phoenix controller and action" do
      assert %Plug.Conn{}
             |> Plug.Conn.put_private(:phoenix_controller, AppsignalPhoenixExample.PageController)
             |> Plug.Conn.put_private(:phoenix_action, :index)
             |> Appsignal.Plug.extract_action() == "AppsignalPhoenixExample.PageController#index"
    end
  end

  describe "extracting request metadata" do
    test "from a Plug conn" do
      assert Appsignal.Plug.extract_meta_data(%Plug.Conn{
               method: "GET",
               request_path: "/foo",
               resp_headers: [{"x-request-id", "kk4hk5sis7c3b56t683nnmdig632c9ot"}],
               status: 200
             }) == %{
               "method" => "GET",
               "path" => "/foo",
               "request_id" => "kk4hk5sis7c3b56t683nnmdig632c9ot",
               "http_status_code" => 200
             }
    end
  end

  describe "extracting sample data" do
    setup do
      %{
        conn: %Plug.Conn{
          params: %{"foo" => "bar"},
          host: "www.example.com",
          method: "GET",
          script_name: ["foo", "bar"],
          request_path: "/foo/bar",
          port: 80,
          query_string: "foo=bar",
          scheme: :http,
          req_headers: [{"accept", "text/html"}]
        }
      }
    end

    test "from a Plug conn", %{conn: conn} do
      assert Appsignal.Plug.extract_sample_data(conn) == %{
               "params" => %{"foo" => "bar"},
               "environment" => %{
                 "host" => "www.example.com",
                 "method" => "GET",
                 "request_path" => "/foo/bar",
                 "port" => 80,
                 "request_uri" => "http://www.example.com:80/foo/bar",
                 "req_headers.accept" => "text/html"
               }
             }
    end

    test "with a param that should be filtered out", %{conn: conn} do
      AppsignalTest.Utils.with_config(%{filter_parameters: ["password"]}, fn ->
        conn = %{conn | params: %{"password" => "secret"}}

        assert %{"params" => %{"password" => "[FILTERED]"}} =
                 Appsignal.Plug.extract_sample_data(conn)
      end)
    end
  end

  describe "extracting request headers" do
    setup do
      [
        conn: %Plug.Conn{
          req_headers: [
            {"content-length", "1024"},
            {"accept", "text/html"},
            {"accept-charset", "utf-8"},
            {"accept-encoding", "gzip, deflate"},
            {"accept-language", "en-us"},
            {"cache-control", "no-cache"},
            {"connection", "keep-alive"},
            {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3..."},
            {"from", "webmaster@example.org"},
            {"referer", "http://localhost:4001/"},
            {"range", "bytes=0-1023"},
            {"cookie", "__ar_v4=U35IKTLTJNEP7GWW6OH3N2%3A20161120%3A90%7CI..."},
            {"x-real-ip", "179.146.231.170"}
          ]
        }
      ]
    end

    test "with header key configuration", %{conn: conn} do
      AppsignalTest.Utils.with_config(%{request_headers: ["content-length"]}, fn ->
        assert Appsignal.Plug.extract_request_headers(conn) == %{
                 "req_headers.content-length" => "1024"
               }
      end)
    end

    test "with fallback header keys", %{conn: conn} do
      assert Appsignal.Plug.extract_request_headers(conn) == %{
               "req_headers.content-length" => "1024",
               "req_headers.accept" => "text/html",
               "req_headers.accept-charset" => "utf-8",
               "req_headers.accept-encoding" => "gzip, deflate",
               "req_headers.accept-language" => "en-us",
               "req_headers.cache-control" => "no-cache",
               "req_headers.connection" => "keep-alive",
               "req_headers.range" => "bytes=0-1023"
             }
    end
  end

  describe "handling errors for a wrapped error" do
    setup do
      transaction = %Appsignal.Transaction{}
      conn = %Plug.Conn{private: %{appsignal_transaction: transaction}}
      kind = :error
      wrapped_reason = :undef
      stack = []

      reason = %Plug.Conn.WrapperError{
        kind: kind,
        reason: wrapped_reason,
        stack: stack,
        conn: conn
      }

      :ok =
        try do
          Appsignal.Plug.handle_error(conn, kind, reason, stack)
        rescue
          Plug.Conn.WrapperError -> :ok
        end

      [conn: conn]
    end

    test "sets the transaction error", %{fake_transaction: fake_transaction} do
      assert [
               {
                 %Appsignal.Transaction{},
                 "UndefinedFunctionError",
                 "undefined function",
                 []
               }
             ] == FakeTransaction.errors(fake_transaction)
    end

    test "sets the transaction's request metdata", %{
      fake_transaction: fake_transaction,
      conn: conn
    } do
      assert conn == FakeTransaction.request_metadata(fake_transaction)
    end
  end

  describe "handling errors for an error with a plug status < 500" do
    test "reraises the error" do
      :ok =
        try do
          transaction = %Appsignal.Transaction{}

          Appsignal.Plug.handle_error(
            %Plug.Conn{private: %{appsignal_transaction: transaction}},
            :error,
            %Plug.BadRequestError{},
            []
          )
        rescue
          Plug.BadRequestError -> :ok
        end
    end
  end

  describe "handling errors for a conn without a transaction" do
    test "reraises the error" do
      :ok =
        try do
          Appsignal.Plug.handle_error(%Plug.Conn{}, :error, :undef, :undef, [])
        rescue
          UndefinedFunctionError -> :ok
        end
    end
  end

  describe "when overriding the AppSignal Plug" do
    setup do
      conn = OverridingAppSignalPlug.call(%Plug.Conn{}, %{})

      [conn: conn]
    end

    test "starts a transaction", %{fake_transaction: fake_transaction} do
      assert FakeTransaction.started_transaction?(fake_transaction)
    end

    test "calls super and returns the conn", %{conn: conn} do
      assert conn.assigns[:called?]
    end

    test "calls the overridden call/2", %{conn: conn} do
      assert conn.assigns[:overridden?]
    end
  end
end


Fix docstring for escape_fragment

Fix docstring for escape_fragment

defmodule IO.ANSI.Sequence do
  @moduledoc false

  defmacro defsequence(name, code) do
    quote do
      name = unquote(name)
      code = unquote(code)

      def name, [], [], do:
        quote do: "\e[#{unquote(code)}m"

      args =
        quote do: [<< unquote(atom_to_binary(name)), rest :: binary >> ]

      defp :escape_sequence, args, [], do:
        quote do: { "\e[#{unquote(code)}m", rest }
    end
  end
end

defmodule IO.ANSI do
  @moduledoc """
  This module provides functionality to render ANSI escape sequences
  (http://en.wikipedia.org/wiki/ANSI_escape_code) —  characters embedded
  in the text used to control formatting, color, and other output options
  on video text terminals.
  """

  import IO.ANSI.Sequence

  @doc """
  Checks whether the default I/O device is a terminal or a file.

  Used to identify whether printing ANSI escape sequences will likely
  be printed as intended.
  """
  @spec terminal? :: boolean
  @spec terminal?(:io.device) :: boolean
  def terminal?(device // :erlang.group_leader) do
    match?({:ok, _}, :io.columns(device))
  end

  @doc "Resets all attributes"
  defsequence :reset, 0

  @doc "Bright (increased intensity) or Bold"
  defsequence :bright, 1

  @doc "Faint (decreased intensity), not widely supported"
  defsequence :faint, 2

  @doc "Italic: on. Not widely supported. Sometimes treated as inverse."
  defsequence :italic, 3

  @doc "Underline: Single"
  defsequence :underline, 4

  @doc "Blink: Slow. Less than 150 per minute"
  defsequence :blink_slow, 5

  @doc "Blink: Rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported"
  defsequence :blink_rapid, 6

  @doc "Image: Negative. Swap foreground and background"
  defsequence :inverse, 7

  @doc "Image: Negative. Swap foreground and background"
  defsequence :reverse, 7

  @doc "Conceal. Not widely supported"
  defsequence :conceal, 8

  @doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported."
  defsequence :crossed_out, 9

  @doc "Sets primary (default) font"
  defsequence :primary_font, 10

  lc font_n inlist [1,2,3,4,5,6,7,8,9] do
    @doc "Sets alternative font #{font_n}"
    defsequence :"font_#{font_n}", font_n + 10
  end

  @doc "Normal color or intensity"
  defsequence :normal, 22

  @doc "Not italic"
  defsequence :not_italic, 23

  @doc "Underline: None"
  defsequence :no_underline, 24

  @doc "Blink: off"
  defsequence :blink_off, 25

  colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
  colors = Enum.zip(0..(length(colors)-1), colors)

  lc { code, color } inlist colors do
    @doc "Sets foreground color to #{color}"
    defsequence color, code + 30

    @doc "Sets background color to #{color}"
    defsequence :"#{color}_background", code + 40
  end

  @doc "Default text color"
  defsequence :default_color, 39

  @doc "Default background color"
  defsequence :default_background, 49

  @doc "Framed"
  defsequence :framed, 51

  @doc "Encircled"
  defsequence :encircled, 52

  @doc "Overlined"
  defsequence :overlined, 53

  @doc "Not framed or encircled"
  defsequence :not_framed_encircled, 54

  @doc "Not overlined"
  defsequence :not_overlined, 55

  # Catch spaces between codes
  defp escape_sequence(<< ?\s, rest :: binary >>) do
    escape_sequence(rest)
  end

  defp escape_sequence(other) do
    [spec|_] = String.split(other, %r/(,|\})/)
    raise ArgumentError, message: "invalid ANSI sequence specification: #{spec}"
  end

  @doc %B"""
  Escapes a string coverting named ANSI sequences into actual ANSI codes.

  The format for referring to sequences is `%{red}` and `%{red,bright}` (for
  multiple sequences).

  It will also append a %{reset} to the string. If you don't want this
  behaviour, use `escape_fragment/1` and `escape_fragment/2`.

  An optional boolean parameter can be passed to enable or disable
  emitting actual ANSI codes. When false, no ANSI codes will emitted.
  By default, standard output will be checked if it is a terminal capable
  of handling these sequences (using `terminal?/0` function)

  ## Example

    iex> IO.ANSI.escape("Hello %{red,bright,green}yes")
    "Hello \e[31m\e[1m\e[32myes\e[0m"
  """
  @spec escape(String.t, emit :: boolean) :: String.t
  def escape(string, emit // terminal?) do
    {rendered, emitted} = do_escape(string, false, emit, false, [])
    if emitted and emit do
      rendered <> reset
    else
      rendered
    end
  end

  @doc %B"""
  Escapes a string by converting named ANSI sequences into actual ANSI codes.

  The format for referring to sequences is `%{red}` and `%{red,bright}` (for
  multiple sequences).

  An optional boolean parameter can be passed to enable or disable
  emitting actual ANSI codes. When false, no ANSI codes will emitted.
  By default, standard output will be checked if it is a terminal capable
  of handling these sequences (using `terminal?/0` function)

  ## Example

    iex> IO.ANSI.escape_fragment("Hello %{red,bright,green}yes")
    "Hello \e[31m\e[1m\e[32myes\e[0m"
    iex> IO.ANSI.escape_fragment("%{reset}bye")
    "\e[0mbye"

  """
  @spec escape_fragment(String.t, emit :: boolean) :: String.t
  def escape_fragment(string, emit // terminal?) do
    {rendered, _emitted} = do_escape(string, false, emit, false, [])
    rendered
  end

  defp do_escape(<< ?%, ?{, rest :: binary >>, false, emit, _emitted, acc) do
    do_escape_sequence(rest, emit, acc)
  end
  defp do_escape(<< ?,, rest :: binary >>, true, emit, _emitted, acc) do
    do_escape_sequence(rest, emit, acc)
  end
  defp do_escape(<< ?\s, rest :: binary >>, true, emit, emitted, acc) do
    do_escape(rest, true, emit, emitted, acc)
  end
  defp do_escape(<< ?}, rest :: binary >>, true, emit, emitted, acc) do
    do_escape(rest, false, emit, emitted, acc)
  end
  defp do_escape(<< x :: [binary, size(1)], rest :: binary>>, false, emit, emitted, acc) do
    do_escape(rest, false, emit, emitted, [x|acc])
  end
  defp do_escape("", false, _emit, emitted, acc) do
    {list_to_binary(Enum.reverse(acc)), emitted}
  end

  defp do_escape_sequence(rest, emit, acc) do
    {code, rest} = escape_sequence(rest)
    if emit do
      acc = [code|acc]
    end
    do_escape(rest, true, emit, true, acc)
  end
end

defmodule IO.ANSI.Sequence do
  @moduledoc false

  defmacro defsequence(name, code) do
    quote do
      name = unquote(name)
      code = unquote(code)

      def name, [], [], do:
        quote do: "\e[#{unquote(code)}m"

      args =
        quote do: [<< unquote(atom_to_binary(name)), rest :: binary >> ]

      defp :escape_sequence, args, [], do:
        quote do: { "\e[#{unquote(code)}m", rest }
    end
  end
end

defmodule IO.ANSI do
  @moduledoc """
  This module provides functionality to render ANSI escape sequences
  (http://en.wikipedia.org/wiki/ANSI_escape_code) —  characters embedded
  in the text used to control formatting, color, and other output options
  on video text terminals.
  """

  import IO.ANSI.Sequence

  @doc """
  Checks whether the default I/O device is a terminal or a file.

  Used to identify whether printing ANSI escape sequences will likely
  be printed as intended.
  """
  @spec terminal? :: boolean
  @spec terminal?(:io.device) :: boolean
  def terminal?(device // :erlang.group_leader) do
    match?({:ok, _}, :io.columns(device))
  end

  @doc "Resets all attributes"
  defsequence :reset, 0

  @doc "Bright (increased intensity) or Bold"
  defsequence :bright, 1

  @doc "Faint (decreased intensity), not widely supported"
  defsequence :faint, 2

  @doc "Italic: on. Not widely supported. Sometimes treated as inverse."
  defsequence :italic, 3

  @doc "Underline: Single"
  defsequence :underline, 4

  @doc "Blink: Slow. Less than 150 per minute"
  defsequence :blink_slow, 5

  @doc "Blink: Rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported"
  defsequence :blink_rapid, 6

  @doc "Image: Negative. Swap foreground and background"
  defsequence :inverse, 7

  @doc "Image: Negative. Swap foreground and background"
  defsequence :reverse, 7

  @doc "Conceal. Not widely supported"
  defsequence :conceal, 8

  @doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported."
  defsequence :crossed_out, 9

  @doc "Sets primary (default) font"
  defsequence :primary_font, 10

  lc font_n inlist [1,2,3,4,5,6,7,8,9] do
    @doc "Sets alternative font #{font_n}"
    defsequence :"font_#{font_n}", font_n + 10
  end

  @doc "Normal color or intensity"
  defsequence :normal, 22

  @doc "Not italic"
  defsequence :not_italic, 23

  @doc "Underline: None"
  defsequence :no_underline, 24

  @doc "Blink: off"
  defsequence :blink_off, 25

  colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
  colors = Enum.zip(0..(length(colors)-1), colors)

  lc { code, color } inlist colors do
    @doc "Sets foreground color to #{color}"
    defsequence color, code + 30

    @doc "Sets background color to #{color}"
    defsequence :"#{color}_background", code + 40
  end

  @doc "Default text color"
  defsequence :default_color, 39

  @doc "Default background color"
  defsequence :default_background, 49

  @doc "Framed"
  defsequence :framed, 51

  @doc "Encircled"
  defsequence :encircled, 52

  @doc "Overlined"
  defsequence :overlined, 53

  @doc "Not framed or encircled"
  defsequence :not_framed_encircled, 54

  @doc "Not overlined"
  defsequence :not_overlined, 55

  # Catch spaces between codes
  defp escape_sequence(<< ?\s, rest :: binary >>) do
    escape_sequence(rest)
  end

  defp escape_sequence(other) do
    [spec|_] = String.split(other, %r/(,|\})/)
    raise ArgumentError, message: "invalid ANSI sequence specification: #{spec}"
  end

  @doc %B"""
  Escapes a string coverting named ANSI sequences into actual ANSI codes.

  The format for referring to sequences is `%{red}` and `%{red,bright}` (for
  multiple sequences).

  It will also append a %{reset} to the string. If you don't want this
  behaviour, use `escape_fragment/1` and `escape_fragment/2`.

  An optional boolean parameter can be passed to enable or disable
  emitting actual ANSI codes. When false, no ANSI codes will emitted.
  By default, standard output will be checked if it is a terminal capable
  of handling these sequences (using `terminal?/0` function)

  ## Example

    iex> IO.ANSI.escape("Hello %{red,bright,green}yes")
    "Hello \e[31m\e[1m\e[32myes\e[0m"
  """
  @spec escape(String.t, emit :: boolean) :: String.t
  def escape(string, emit // terminal?) do
    {rendered, emitted} = do_escape(string, false, emit, false, [])
    if emitted and emit do
      rendered <> reset
    else
      rendered
    end
  end

  @doc %B"""
  Escapes a string coverting named ANSI sequences into actual ANSI codes.

  The format for referring sequences is `%{red}` and `%{red,bright}` (for
  multiple sequences)

  An optional boolean parameter can be passed to enable or disable
  emitting actual ANSI codes. When false, no ANSI codes will emitted.
  By default, standard output will be checked if it is a terminal capable
  of handling these sequences (using `terminal?/0` function)

  ## Example

    iex> IO.ANSI.escape("Hello %{red,bright,green}yes")
    "Hello \e[31m\e[1m\e[32myes\e[0m"

  """
  @spec escape_fragment(String.t, emit :: boolean) :: String.t
  def escape_fragment(string, emit // terminal?) do
    {rendered, _emitted} = do_escape(string, false, emit, false, [])
    rendered
  end

  defp do_escape(<< ?%, ?{, rest :: binary >>, false, emit, _emitted, acc) do
    do_escape_sequence(rest, emit, acc)
  end
  defp do_escape(<< ?,, rest :: binary >>, true, emit, _emitted, acc) do
    do_escape_sequence(rest, emit, acc)
  end
  defp do_escape(<< ?\s, rest :: binary >>, true, emit, emitted, acc) do
    do_escape(rest, true, emit, emitted, acc)
  end
  defp do_escape(<< ?}, rest :: binary >>, true, emit, emitted, acc) do
    do_escape(rest, false, emit, emitted, acc)
  end
  defp do_escape(<< x :: [binary, size(1)], rest :: binary>>, false, emit, emitted, acc) do
    do_escape(rest, false, emit, emitted, [x|acc])
  end
  defp do_escape("", false, _emit, emitted, acc) do
    {list_to_binary(Enum.reverse(acc)), emitted}
  end

  defp do_escape_sequence(rest, emit, acc) do
    {code, rest} = escape_sequence(rest)
    if emit do
      acc = [code|acc]
    end
    do_escape(rest, true, emit, true, acc)
  end
end


Fix IO.ANSI docs

Fix IO.ANSI docs

defmodule IO.ANSI.Sequence do
  @moduledoc false

  defmacro defsequence(name, code // "", terminator // "m") do
    quote bind_quoted: [name: name, code: code, terminator: terminator] do
      def unquote(name)() do
        "\e[#{unquote(code)}#{unquote(terminator)}"
      end

      defp escape_sequence(<< unquote(atom_to_binary(name)), rest :: binary >>) do
        { "\e[#{unquote(code)}#{unquote(terminator)}", rest }
      end
    end
  end
end

defmodule IO.ANSI do
  @moduledoc """
  This module provides functionality to render ANSI escape sequences
  (http://en.wikipedia.org/wiki/ANSI_escape_code) —  characters embedded
  in the text used to control formatting, color, and other output options
  on video text terminals.
  """

  import IO.ANSI.Sequence

  @doc """
  Checks whether the default I/O device is a terminal or a file.

  Used to identify whether printing ANSI escape sequences will likely
  be printed as intended.
  """
  @spec terminal? :: boolean
  @spec terminal?(:io.device) :: boolean
  def terminal?(device // :erlang.group_leader) do
    match?({:ok, _}, :io.columns(device))
  end

  @doc "Resets all attributes"
  defsequence :reset, 0

  @doc "Bright (increased intensity) or Bold"
  defsequence :bright, 1

  @doc "Faint (decreased intensity), not widely supported"
  defsequence :faint, 2

  @doc "Italic: on. Not widely supported. Sometimes treated as inverse."
  defsequence :italic, 3

  @doc "Underline: Single"
  defsequence :underline, 4

  @doc "Blink: Slow. Less than 150 per minute"
  defsequence :blink_slow, 5

  @doc "Blink: Rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported"
  defsequence :blink_rapid, 6

  @doc "Image: Negative. Swap foreground and background"
  defsequence :inverse, 7

  @doc "Image: Negative. Swap foreground and background"
  defsequence :reverse, 7

  @doc "Conceal. Not widely supported"
  defsequence :conceal, 8

  @doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported."
  defsequence :crossed_out, 9

  @doc "Sets primary (default) font"
  defsequence :primary_font, 10

  lc font_n inlist [1, 2, 3, 4, 5, 6, 7, 8, 9] do
    @doc "Sets alternative font #{font_n}"
    defsequence :"font_#{font_n}", font_n + 10
  end

  @doc "Normal color or intensity"
  defsequence :normal, 22

  @doc "Not italic"
  defsequence :not_italic, 23

  @doc "Underline: None"
  defsequence :no_underline, 24

  @doc "Blink: off"
  defsequence :blink_off, 25

  colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
  colors = Enum.zip(0..(length(colors)-1), colors)

  lc { code, color } inlist colors do
    @doc "Sets foreground color to #{color}"
    defsequence color, code + 30

    @doc "Sets background color to #{color}"
    defsequence :"#{color}_background", code + 40
  end

  @doc "Default text color"
  defsequence :default_color, 39

  @doc "Default background color"
  defsequence :default_background, 49

  @doc "Framed"
  defsequence :framed, 51

  @doc "Encircled"
  defsequence :encircled, 52

  @doc "Overlined"
  defsequence :overlined, 53

  @doc "Not framed or encircled"
  defsequence :not_framed_encircled, 54

  @doc "Not overlined"
  defsequence :not_overlined, 55

  @doc "Send cursor home"
  defsequence :home, "", "H"

  @doc "Clear screen"
  defsequence :clear, "2", "J"

  # Catch spaces between codes
  defp escape_sequence(<< ?\s, rest :: binary >>) do
    escape_sequence(rest)
  end

  defp escape_sequence(other) do
    [spec|_] = String.split(other, %r/(,|\})/)
    raise ArgumentError, message: "invalid ANSI sequence specification: #{spec}"
  end

  @doc %S"""
  Escapes a string by converting named ANSI sequences into actual ANSI codes.

  The format for referring to sequences is `%{red}` and `%{red,bright}` (for
  multiple sequences).

  It will also append a %{reset} to the string. If you don't want this
  behaviour, use `escape_fragment/1` and `escape_fragment/2`.

  An optional boolean parameter can be passed to enable or disable
  emitting actual ANSI codes. When false, no ANSI codes will emitted.
  By default, standard output will be checked if it is a terminal capable
  of handling these sequences (using `terminal?/0` function)

  ## Example

      iex> IO.ANSI.escape("Hello %{red,bright,green}yes", true)
      "Hello \e[31m\e[1m\e[32myes\e[0m"

  """
  @spec escape(String.t, emit :: boolean) :: String.t
  def escape(string, emit // terminal?) do
    {rendered, emitted} = do_escape(string, false, emit, false, [])
    if emitted and emit do
      rendered <> reset
    else
      rendered
    end
  end

  @doc %S"""
  Escapes a string by converting named ANSI sequences into actual ANSI codes.

  The format for referring to sequences is `%{red}` and `%{red,bright}` (for
  multiple sequences).

  An optional boolean parameter can be passed to enable or disable
  emitting actual ANSI codes. When false, no ANSI codes will emitted.
  By default, standard output will be checked if it is a terminal capable
  of handling these sequences (using `terminal?/0` function)

  ## Example

    iex> IO.ANSI.escape_fragment("Hello %{red,bright,green}yes", true)
    "Hello \e[31m\e[1m\e[32myes"
    iex> IO.ANSI.escape_fragment("%{reset}bye", true)
    "\e[0mbye"

  """
  @spec escape_fragment(String.t, emit :: boolean) :: String.t
  def escape_fragment(string, emit // terminal?) do
    {rendered, _emitted} = do_escape(string, false, emit, false, [])
    rendered
  end

  defp do_escape(<< ?%, ?{, rest :: binary >>, false, emit, _emitted, acc) do
    do_escape_sequence(rest, emit, acc)
  end
  defp do_escape(<< ?,, rest :: binary >>, true, emit, _emitted, acc) do
    do_escape_sequence(rest, emit, acc)
  end
  defp do_escape(<< ?\s, rest :: binary >>, true, emit, emitted, acc) do
    do_escape(rest, true, emit, emitted, acc)
  end
  defp do_escape(<< ?}, rest :: binary >>, true, emit, emitted, acc) do
    do_escape(rest, false, emit, emitted, acc)
  end
  defp do_escape(<< x :: [binary, size(1)], rest :: binary>>, false, emit, emitted, acc) do
    do_escape(rest, false, emit, emitted, [x|acc])
  end
  defp do_escape("", false, _emit, emitted, acc) do
    {iolist_to_binary(Enum.reverse(acc)), emitted}
  end

  defp do_escape_sequence(rest, emit, acc) do
    {code, rest} = escape_sequence(rest)
    if emit do
      acc = [code|acc]
    end
    do_escape(rest, true, emit, true, acc)
  end
end