Remove unused mix mocha task

Remove unused mix mocha task

defmodule Mix.Tasks.Mocha do
  use Mix.Task

  @shortdoc "Runs javascript unit tests via the mocha executable"

  def run(_args) do
    System.cmd("mocha", [], into: IO.stream(:stdio, :line), env: [{"NODE_ENV", "test"}])
  end

  defmodule Watch do
    @shortdoc "Runs javascript unit tests on file change via the mocha executable"

    def run(_args) do
      System.cmd("mocha", ["--watch"], into: IO.stream(:stdio, :line), env: [{"NODE_ENV", "test"}])
    end
  end
end


Little refactoring

Little refactoring

defmodule ExUnit.DocTest do
  @moduledoc """
  ExUnit.DocTest implements functionality similar to [Python's
  doctest](http://docs.python.org/2/library/doctest.html).

  In a nutshell, it allows us to generate tests from the code
  examples existing in module/function/macro's documentation.
  In order to do that, one needs to invoke `doctest/1` macro
  from their test case and write their examples according
  to some guidelines.

  The syntax for examples is as follows. Every new test starts
  on a new line, with an "iex>" prefix. Multiline expressions
  can be employed if the following lines start with either
  "...>" (recommended) or "iex>" prefix.

  The expected result should start at the next line after "iex>"
  or "...>" line(s) and is terminated either by a newline, new
  "iex>" prefix or end of the string literal.

  ## Examples

  Currently, the only way to run doctests is to include them into
  an ExUnit case with a `doctest` macro:

      defmodule MyModule.Test do
        use ExUnit.Case, async: true
        doctest MyModule
      end

  The `doctest` macro is going to loop all functions and macros
  defined in `MyModule`, parsing their documentation in search for
  code examples.

  A very basic example is:

      iex> 1+1
      2

  Multiline is also supported:

      iex> Enum.map [1,2,3], fn(x) ->
      ...>   x * 2
      ...> end
      [2,4,6]

  Similarly to iex you can use numbers in your "prompts":

      iex(1)> [1+2,
      ...(1)>  3]
      [3,3]

  This is useful in two use cases:

  * Being able to refer to specific numbered scenarios
  * Copy-pasting examples from an actual iex sessions

  We also allow you to select or skip some functions when calling
  `doctest`. See its documentation documentation for more info.

  ## Exceptions

  You can also showcase expressions raising an exception, for example:

      iex(1)> binary_to_atom((fn() -> 1 end).())
      ** (ArgumentError) argument error

  What DocTest will be looking for is a line starting with "** (" and it
  will parse it accordingly to extract the exception name and the message.
  At this moment, the exception parser would make the parser treat the next
  line as a start of a completely new expression (if it is prefixed with iex>)
  or a no-op line with documentation. Thus, multiline messages are not
  supported.

  ## When not to use doctest

  In general, doctests are not recommended when your code examples contain
  side effects. For example, if a doctest prints to standard output, doctest
  will not try to capture the output.

  Similarly, doctest does not run in any kind of sandbox. So any module
  defined in a code example is going to linger throughout the whole test suite
  run.
  """

  defexception Error, message: nil

  defrecord Test, fun_arity: nil, line: nil, exprs: []

  @doc """
  This macro is used to generate ExUnit test cases for doctests.

  There are three ways this macro can be used:

  * `doctest(Module)` — will generate tests for all doctests found
     in the module `Module`

  Options can also be supplied:

  * `:except` — generate tests for all functions except those listed
                (list of `{function, arity}` tuples)

  * `:only` — generate tests only forfunctions listed
              (list of `{function, arity}` tuples)

  * `:import` — when true, one can test a function defined in the module
                without referring to the module name. However, this is not
                feasible when there is a clash with a number module like
                Kernel. In these cases, `import` should be set to `false`
                and a full `M.f` construct should be used.

  ## Examples

      doctest MyModule, except: [trick_fun: 1]

  This macro is auto-imported into every `ExUnit.Case`.
  """
  defmacro doctest(mod, opts // []) do
    quote do
      lc { name, test } inlist unquote(__MODULE__).__doctests__(unquote(mod), unquote(opts)) do
        def name, quote(do: [_]), [], do: test
      end
    end
  end

  @doc false
  def __doctests__(module, opts) do
    only   = opts[:only] || []
    except = opts[:except] || []
    do_import = Keyword.get(opts, :import, false)

    tests = Enum.filter(extract(module), fn(test) ->
      fa = test.fun_arity
      Enum.all?(except, &1 != fa) and Enum.all?(only, &1 == fa)
    end)

    Enum.map_reduce(tests, 1, fn(test, acc) ->
      { compile_test(test, module, do_import, acc), acc + 1 }
    end) |> elem(0)
  end

  ## Compilation of extracted tests

  defp compile_test(test, module, do_import, n) do
    { test_name(test, module, n), test_content(test, module, do_import) }
  end

  defp test_name(Test[fun_arity: nil], m, n) do
    :"test moduledoc at #{inspect m} (#{n})"
  end

  defp test_name(Test[fun_arity: { f, a }], m, n) do
    :"test doc at #{inspect m}.#{f}/#{a} (#{n})"
  end

  defp test_content(Test[exprs: exprs, line: line], module, do_import) do
    #IO.puts "Testing tests:"
    #Enum.each exprs, fn { expr, expected } ->
      #IO.puts "test '#{expr}' with expectation #{inspect expected}"
    #end
    #IO.puts ""

    file     = module.__info__(:compile)[:source]
    location = [line: line, file: Path.relative_to(file, System.cwd!)]
    stack    = Macro.escape [{ module, :__MODULE__, 0, location }]

    exc_filter_fn = (function do
      { _, {:error, _, _} } -> true
      _ -> false
    end)

    exceptions_num = Enum.count exprs, exc_filter_fn
    if exceptions_num > 1 do
      # FIXME: stacktrace pointing to the doctest?
      raise Error, message: "Multiple exceptions in one doctest case are not supported"
    end

    { tests, whole_expr } = Enum.map_reduce exprs, "", fn {expr, expected}, acc ->
      { test_case_content(expr, expected, module, line, file, stack), acc <> expr <> "\n" }
    end
    exception_expr = Enum.find(exprs, exc_filter_fn)

    if nil?(exception_expr) do
      quote do
        unquote_splicing(test_import(module, do_import))
        try do
          # Put all tests into one context
          unquote_splicing(tests)
        rescue
          e in [ExUnit.ExpectationError] ->
            raise e, [], unquote(stack)
          actual ->
            raise ExUnit.ExpectationError,
              [ prelude: "Expected doctest",
                description: unquote(whole_expr),
                expected: "without an exception",
                reason: "complete",
                actual: inspect(actual) ],
              unquote(stack)
        end
      end
    else
      { expr, {:error, exception, message} } = exception_expr
      quote do
        unquote_splicing(test_import(module, do_import))
        try do
          # Put all tests into one context
          unquote_splicing(tests)
        rescue
          e in [ExUnit.ExpectationError] ->
            case e.reason do
              "evaluate to" ->
                raise e, [], unquote(stack)
              "raise" ->
                raise(e)
            end

          error in [unquote(exception)] ->
            unless error.message == unquote(message) do
              raise ExUnit.ExpectationError,
                [ prelude: "Expected doctest",
                  description: unquote(expr),
                  expected: "#{inspect unquote(exception)} with message #{inspect unquote(message)}",
                  reason: "raise",
                  actual: inspect(error) ],
                unquote(stack)
            end

          actual ->
            raise ExUnit.ExpectationError,
              [ prelude: "Expected doctest",
                description: unquote(whole_expr),
                expected: "#{inspect unquote(exception)}",
                reason: "complete or raise",
                actual: inspect(actual) ],
              unquote(stack)
        end
      end
    end
  end

  defp test_case_content(expr, { :test, expected }, module, line, file, stack) do
    expr_ast     = string_to_ast(module, line, file, expr)
    expected_ast = string_to_ast(module, line, file, expected)

    quote do
      v = unquote(expected_ast)
      case unquote(expr_ast) do
        ^v -> :ok
        actual ->
          raise ExUnit.ExpectationError,
            [ prelude: "Expected doctest",
              description: unquote(expr),
              expected: inspect(v),
              reason: "evaluate to",
              actual: inspect(actual) ],
            unquote(stack)
      end
    end
  end

  defp test_case_content(expr, { :error, exception, _ }, module, line, file, stack) do
    expr_ast = string_to_ast(module, line, file, expr)

    quote do
      v = unquote(expr_ast)
      raise ExUnit.ExpectationError,
        [ prelude: "Expected doctest",
          description: unquote(expr),
          expected: "#{inspect unquote(exception)}[]",
          reason: "raise",
          actual: inspect(v) ],
        unquote(stack)
    end
  end

  defp test_import(_mod, false), do: []
  defp test_import(mod, _) do
    [quote do: import(unquote(mod))]
  end

  defp string_to_ast(module, line, file, expr) do
    location = [line: line, file: Path.relative_to(file, System.cwd!)]
    stack    = Macro.escape [{ module, :__MODULE__, 0, location }]
    try do
      Code.string_to_ast!(expr, line: line, file: file)
    rescue e ->
      quote do
        raise ExUnit.ExpectationError,
          [ prelude: "Expected doctest",
            description: unquote(expr),
            expected: "successfully",
            reason: "compile",
            actual: unquote("** #{inspect e.__record__(:name)} #{e.message}") ],
          unquote(stack)
      end
    end
  end

  ## Extraction of the tests

  defp extract(module) do
    moduledocs = extract_from_moduledoc(module.__info__(:moduledoc))

    docs = lc doc inlist module.__info__(:docs) do
      extract_from_doc(doc)
    end |> List.concat

    moduledocs ++ docs
  end

  defp extract_from_moduledoc({_, negative}) when negative in [false, nil], do: []

  defp extract_from_moduledoc({line, doc}) do
    extract_tests(line, doc)
  end

  defp extract_from_doc({_, _, _, _, negative}) when negative in [false, nil], do: []

  defp extract_from_doc({ fa, line, _, _, doc}) do
    lc test inlist extract_tests(line, doc) do
      test.fun_arity(fa)
    end
  end

  defp extract_tests(line, doc) do
    lines = String.split(doc, %r/\n/) |> Enum.map(function(String.strip/1))
    extract_tests(lines, line, "", "", [], true)
  end

  defp extract_tests([], _line, "", "", [], _) do
    []
  end

  defp extract_tests([], _line, "", "", acc, _) do
    Enum.reverse(reverse_last_test(acc))
  end

  # End of input and we've still got a test pending.
  defp extract_tests([], _, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], _) do
    test = test.exprs([{ expr_acc, {:test, expected_acc} } | exprs])
    Enum.reverse(reverse_last_test([test|t]))
  end

  # We've encountered the next test on an adjacent line. Put them into one group.
  defp extract_tests([<< "iex>", _ :: binary>>|_] = list, line, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], newtest) when expr_acc != "" and expected_acc != "" do
    test = test.exprs([{ expr_acc, {:test, expected_acc} } | exprs])
    extract_tests(list, line, "", "", [test|t], newtest)
  end

  # Store expr_acc and start a new test case.
  defp extract_tests([<< "iex>", string :: binary>>|lines], line, "", expected_acc, acc, true) do
    acc = reverse_last_test(acc)
    test = Test[line: line]
    extract_tests(lines, line, string, expected_acc, [test|acc], false)
  end

  # Store expr_acc.
  defp extract_tests([<< "iex>", string :: binary>>|lines], line, "", expected_acc, acc, false) do
    extract_tests(lines, line, string, expected_acc, acc, false)
  end

  # Still gathering expr_acc. Synonym for the next clause.
  defp extract_tests([<< "iex>", string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc, newtest)
  end

  # Still gathering expr_acc. Synonym for the previous clause.
  defp extract_tests([<< "...>", string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) when expr_acc != "" do
    extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc, newtest)
  end

  # Expression numbers are simply skipped.
  defp extract_tests([<< "iex(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(["iex" <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc, newtest)
  end

  # Expression numbers are simply skipped redux.
  defp extract_tests([<< "...(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(["..." <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc, newtest)
  end

  # Skip empty or documentation line.
  defp extract_tests([_|lines], line, "", "", acc, _) do
    extract_tests(lines, line, "", "", acc, true)
  end

  # Encountered an empty line, store pending test
  defp extract_tests([""|lines], line, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], _) do
    test = test.exprs([{ expr_acc, {:test, expected_acc} } | exprs])
    extract_tests(lines, line,  "", "", [test|t], true)
  end

  # Exception test.
  defp extract_tests([<< "** (", string :: binary >>|lines], line, expr_acc, "", [test=Test[exprs: exprs]|t], newtest) do
    test = test.exprs([{ expr_acc, extract_error(string, "") } | exprs])
    extract_tests(lines, line,  "", "", [test|t], newtest)
  end

  # Finally, parse expected_acc.
  defp extract_tests([expected|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(lines, line, expr_acc, expected_acc <> "\n" <> expected, acc, newtest)
  end

  defp extract_error(<< ")", t :: binary >>, acc) do
    { :error, Module.concat([acc]), String.strip(t) }
  end

  defp extract_error(<< h, t :: binary >>, acc) do
    extract_error(t, << acc :: binary, h >>)
  end

  defp skip_iex_number(<< ")", ">", string :: binary >>) do
    ">" <> string
  end

  defp skip_iex_number(<< _ :: 8, string :: binary >>) do
    skip_iex_number(string)
  end

  defp reverse_last_test([]), do: []
  defp reverse_last_test([test=Test[exprs: exprs] | t]) do
    test = test.exprs(Enum.reverse(exprs))
    [test | t]
  end
end

defmodule ExUnit.DocTest do
  @moduledoc """
  ExUnit.DocTest implements functionality similar to [Python's
  doctest](http://docs.python.org/2/library/doctest.html).

  In a nutshell, it allows us to generate tests from the code
  examples existing in module/function/macro's documentation.
  In order to do that, one needs to invoke `doctest/1` macro
  from their test case and write their examples according
  to some guidelines.

  The syntax for examples is as follows. Every new test starts
  on a new line, with an "iex>" prefix. Multiline expressions
  can be employed if the following lines start with either
  "...>" (recommended) or "iex>" prefix.

  The expected result should start at the next line after "iex>"
  or "...>" line(s) and is terminated either by a newline, new
  "iex>" prefix or end of the string literal.

  ## Examples

  Currently, the only way to run doctests is to include them into
  an ExUnit case with a `doctest` macro:

      defmodule MyModule.Test do
        use ExUnit.Case, async: true
        doctest MyModule
      end

  The `doctest` macro is going to loop all functions and macros
  defined in `MyModule`, parsing their documentation in search for
  code examples.

  A very basic example is:

      iex> 1+1
      2

  Multiline is also supported:

      iex> Enum.map [1,2,3], fn(x) ->
      ...>   x * 2
      ...> end
      [2,4,6]

  Similarly to iex you can use numbers in your "prompts":

      iex(1)> [1+2,
      ...(1)>  3]
      [3,3]

  This is useful in two use cases:

  * Being able to refer to specific numbered scenarios
  * Copy-pasting examples from an actual iex sessions

  We also allow you to select or skip some functions when calling
  `doctest`. See its documentation documentation for more info.

  ## Exceptions

  You can also showcase expressions raising an exception, for example:

      iex(1)> binary_to_atom((fn() -> 1 end).())
      ** (ArgumentError) argument error

  What DocTest will be looking for is a line starting with "** (" and it
  will parse it accordingly to extract the exception name and the message.
  At this moment, the exception parser would make the parser treat the next
  line as a start of a completely new expression (if it is prefixed with iex>)
  or a no-op line with documentation. Thus, multiline messages are not
  supported.

  ## When not to use doctest

  In general, doctests are not recommended when your code examples contain
  side effects. For example, if a doctest prints to standard output, doctest
  will not try to capture the output.

  Similarly, doctest does not run in any kind of sandbox. So any module
  defined in a code example is going to linger throughout the whole test suite
  run.
  """

  defexception Error, message: nil

  defrecord Test, fun_arity: nil, line: nil, exprs: []

  @doc """
  This macro is used to generate ExUnit test cases for doctests.

  There are three ways this macro can be used:

  * `doctest(Module)` — will generate tests for all doctests found
     in the module `Module`

  Options can also be supplied:

  * `:except` — generate tests for all functions except those listed
                (list of `{function, arity}` tuples)

  * `:only` — generate tests only forfunctions listed
              (list of `{function, arity}` tuples)

  * `:import` — when true, one can test a function defined in the module
                without referring to the module name. However, this is not
                feasible when there is a clash with a number module like
                Kernel. In these cases, `import` should be set to `false`
                and a full `M.f` construct should be used.

  ## Examples

      doctest MyModule, except: [trick_fun: 1]

  This macro is auto-imported into every `ExUnit.Case`.
  """
  defmacro doctest(mod, opts // []) do
    quote do
      lc { name, test } inlist unquote(__MODULE__).__doctests__(unquote(mod), unquote(opts)) do
        def name, quote(do: [_]), [], do: test
      end
    end
  end

  @doc false
  def __doctests__(module, opts) do
    only   = opts[:only] || []
    except = opts[:except] || []
    do_import = Keyword.get(opts, :import, false)

    tests = Enum.filter(extract(module), fn(test) ->
      fa = test.fun_arity
      Enum.all?(except, &1 != fa) and Enum.all?(only, &1 == fa)
    end)

    Enum.map_reduce(tests, 1, fn(test, acc) ->
      { compile_test(test, module, do_import, acc), acc + 1 }
    end) |> elem(0)
  end

  ## Compilation of extracted tests

  defp compile_test(test, module, do_import, n) do
    { test_name(test, module, n), test_content(test, module, do_import) }
  end

  defp test_name(Test[fun_arity: nil], m, n) do
    :"test moduledoc at #{inspect m} (#{n})"
  end

  defp test_name(Test[fun_arity: { f, a }], m, n) do
    :"test doc at #{inspect m}.#{f}/#{a} (#{n})"
  end

  defp test_content(Test[exprs: exprs, line: line], module, do_import) do
    #IO.puts "Testing tests:"
    #Enum.each exprs, fn { expr, expected } ->
      #IO.puts "test '#{expr}' with expectation #{inspect expected}"
    #end
    #IO.puts ""

    file     = module.__info__(:compile)[:source]
    location = [line: line, file: Path.relative_to(file, System.cwd!)]
    stack    = Macro.escape [{ module, :__MODULE__, 0, location }]

    exc_filter_fn = (function do
      { _, {:error, _, _} } -> true
      _ -> false
    end)

    exceptions_num = Enum.count exprs, exc_filter_fn
    if exceptions_num > 1 do
      # FIXME: stacktrace pointing to the doctest?
      raise Error, message: "Multiple exceptions in one doctest case are not supported"
    end

    { tests, whole_expr } = Enum.map_reduce exprs, "", fn {expr, expected}, acc ->
      { test_case_content(expr, expected, module, line, file, stack), acc <> expr <> "\n" }
    end
    exception_expr = Enum.find(exprs, exc_filter_fn)

    if nil?(exception_expr) do
      quote do
        unquote_splicing(test_import(module, do_import))
        try do
          # Put all tests into one context
          unquote_splicing(tests)
        rescue
          e in [ExUnit.ExpectationError] ->
            raise e, [], unquote(stack)
          actual ->
            raise ExUnit.ExpectationError,
              [ prelude: "Expected doctest",
                description: unquote(whole_expr),
                expected: "without an exception",
                reason: "complete",
                actual: inspect(actual) ],
              unquote(stack)
        end
      end
    else
      { expr, {:error, exception, message} } = exception_expr
      quote do
        unquote_splicing(test_import(module, do_import))
        try do
          # Put all tests into one context
          unquote_splicing(tests)
        rescue
          e in [ExUnit.ExpectationError] ->
            case e.reason do
              "evaluate to" ->
                raise e, [], unquote(stack)
              "raise" ->
                raise(e)
            end

          error in [unquote(exception)] ->
            unless error.message == unquote(message) do
              raise ExUnit.ExpectationError,
                [ prelude: "Expected doctest",
                  description: unquote(expr),
                  expected: "#{inspect unquote(exception)} with message #{inspect unquote(message)}",
                  reason: "raise",
                  actual: inspect(error) ],
                unquote(stack)
            end

          actual ->
            raise ExUnit.ExpectationError,
              [ prelude: "Expected doctest",
                description: unquote(whole_expr),
                expected: "#{inspect unquote(exception)}",
                reason: "complete or raise",
                actual: inspect(actual) ],
              unquote(stack)
        end
      end
    end
  end

  defp test_case_content(expr, { :test, expected }, module, line, file, stack) do
    expr_ast     = string_to_ast(module, line, file, expr)
    expected_ast = string_to_ast(module, line, file, expected)

    quote do
      v = unquote(expected_ast)
      case unquote(expr_ast) do
        ^v -> :ok
        actual ->
          raise ExUnit.ExpectationError,
            [ prelude: "Expected doctest",
              description: unquote(expr),
              expected: inspect(v),
              reason: "evaluate to",
              actual: inspect(actual) ],
            unquote(stack)
      end
    end
  end

  defp test_case_content(expr, { :error, exception, _ }, module, line, file, stack) do
    expr_ast = string_to_ast(module, line, file, expr)

    quote do
      v = unquote(expr_ast)
      raise ExUnit.ExpectationError,
        [ prelude: "Expected doctest",
          description: unquote(expr),
          expected: "#{inspect unquote(exception)}[]",
          reason: "raise",
          actual: inspect(v) ],
        unquote(stack)
    end
  end

  defp test_import(_mod, false), do: []
  defp test_import(mod, _) do
    [quote do: import(unquote(mod))]
  end

  defp string_to_ast(module, line, file, expr) do
    location = [line: line, file: Path.relative_to(file, System.cwd!)]
    stack    = Macro.escape [{ module, :__MODULE__, 0, location }]
    try do
      Code.string_to_ast!(expr, line: line, file: file)
    rescue e ->
      quote do
        raise ExUnit.ExpectationError,
          [ prelude: "Expected doctest",
            description: unquote(expr),
            expected: "successfully",
            reason: "compile",
            actual: unquote("** #{inspect e.__record__(:name)} #{e.message}") ],
          unquote(stack)
      end
    end
  end

  ## Extraction of the tests

  defp extract(module) do
    moduledocs = extract_from_moduledoc(module.__info__(:moduledoc))

    docs = lc doc inlist module.__info__(:docs) do
      extract_from_doc(doc)
    end |> List.concat

    moduledocs ++ docs
  end

  defp extract_from_moduledoc({_, negative}) when negative in [false, nil], do: []

  defp extract_from_moduledoc({line, doc}) do
    extract_tests(line, doc)
  end

  defp extract_from_doc({_, _, _, _, negative}) when negative in [false, nil], do: []

  defp extract_from_doc({ fa, line, _, _, doc}) do
    lc test inlist extract_tests(line, doc) do
      test.fun_arity(fa)
    end
  end

  defp extract_tests(line, doc) do
    lines = String.split(doc, %r/\n/) |> Enum.map(function(String.strip/1))
    extract_tests(lines, line, "", "", [], true)
  end

  defp extract_tests([], _line, "", "", [], _) do
    []
  end

  defp extract_tests([], _line, "", "", [test=Test[exprs: exprs]|t], _) do
    test = test.exprs(Enum.reverse(exprs))
    Enum.reverse([test|t])
  end

  # End of input and we've still got a test pending.
  defp extract_tests([], _, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], _) do
    test = test.exprs(Enum.reverse([{ expr_acc, {:test, expected_acc} } | exprs]))
    Enum.reverse([test|t])
  end

  # We've encountered the next test on an adjacent line. Put them into one group.
  defp extract_tests([<< "iex>", _ :: binary>>|_] = list, line, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], newtest) when expr_acc != "" and expected_acc != "" do
    test = test.exprs([{ expr_acc, {:test, expected_acc} } | exprs])
    extract_tests(list, line, "", "", [test|t], newtest)
  end

  # Store expr_acc.
  defp extract_tests([<< "iex>", string :: binary>>|lines], line, "", expected_acc, acc, newtest) do
    if newtest do
      if match?([test=Test[exprs: exprs] | t], acc) do
        acc = [test.exprs(Enum.reverse(exprs)) | t]
      end
      acc = [Test[line: line]|acc]
    end
    extract_tests(lines, line, string, expected_acc, acc, false)
  end

  # Still gathering expr_acc. Synonym for the next clause.
  defp extract_tests([<< "iex>", string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc, newtest)
  end

  # Still gathering expr_acc. Synonym for the previous clause.
  defp extract_tests([<< "...>", string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) when expr_acc != "" do
    extract_tests(lines, line, expr_acc <> "\n" <> string, expected_acc, acc, newtest)
  end

  # Expression numbers are simply skipped.
  defp extract_tests([<< "iex(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(["iex" <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc, newtest)
  end

  # Expression numbers are simply skipped redux.
  defp extract_tests([<< "...(", _ :: 8, string :: binary>>|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(["..." <> skip_iex_number(string)|lines], line, expr_acc, expected_acc, acc, newtest)
  end

  # Skip empty or documentation line.
  defp extract_tests([_|lines], line, "", "", acc, _) do
    extract_tests(lines, line, "", "", acc, true)
  end

  # Encountered an empty line, store pending test
  defp extract_tests([""|lines], line, expr_acc, expected_acc, [test=Test[exprs: exprs]|t], _) do
    test = test.exprs([{ expr_acc, {:test, expected_acc} } | exprs])
    extract_tests(lines, line,  "", "", [test|t], true)
  end

  # Exception test.
  defp extract_tests([<< "** (", string :: binary >>|lines], line, expr_acc, "", [test=Test[exprs: exprs]|t], newtest) do
    test = test.exprs([{ expr_acc, extract_error(string, "") } | exprs])
    extract_tests(lines, line,  "", "", [test|t], newtest)
  end

  # Finally, parse expected_acc.
  defp extract_tests([expected|lines], line, expr_acc, expected_acc, acc, newtest) do
    extract_tests(lines, line, expr_acc, expected_acc <> "\n" <> expected, acc, newtest)
  end

  defp extract_error(<< ")", t :: binary >>, acc) do
    { :error, Module.concat([acc]), String.strip(t) }
  end

  defp extract_error(<< h, t :: binary >>, acc) do
    extract_error(t, << acc :: binary, h >>)
  end

  defp skip_iex_number(<< ")", ">", string :: binary >>) do
    ">" <> string
  end

  defp skip_iex_number(<< _ :: 8, string :: binary >>) do
    skip_iex_number(string)
  end
end


Skip metadata retrieval for prepared queries

defmodule Xandra.Protocol do
  use Bitwise

  alias Xandra.{Query, Result, Error}

  def encode_query(statement, values, opts) do
    <<byte_size(statement)::32>> <> statement <>
      encode_params(values, opts, false)
  end

  def encode_prepared_query(id, values, opts) do
    <<byte_size(id)::16>> <> id <>
      encode_params(values, opts, true)
  end

  @consistency_levels %{
    0x0000 => :any,
    0x0001 => :one,
    0x0002 => :two,
    0x0003 => :three,
    0x0004 => :quorum,
    0x0005 => :all,
    0x0006 => :local_quorum,
    0x0007 => :each_quorum,
    0x0008 => :serial,
    0x0009 => :local_serial,
    0x000A => :local_one,
  }

  for {spec, level} <- @consistency_levels do
    defp encode_consistency_level(unquote(level)) do
      <<unquote(spec)::16>>
    end
  end

  defp set_query_values(mask, values) do
    cond do
      values == [] ->
        mask
      is_list(values) ->
        mask ||| 0x01
      map_size(values) == 0 ->
        mask
      is_map(values) ->
        mask ||| 0x01 ||| 0x40
    end
  end

  defp set_metadata_presence(mask, skip_metadata?) do
    if skip_metadata? do
      mask ||| 0x02
    else
      mask
    end
  end

  defp encode_params(values, opts, skip_metadata?) do
    consistency = Keyword.get(opts, :consistency, :one)
    page_size = Keyword.get(opts, :page_size, 10_000)

    flag_mask =
      set_query_values(0x00, values)
      |> bor(0x04)
      |> set_metadata_presence(skip_metadata?)

    encode_consistency_level(consistency) <>
      <<flag_mask>> <>
      encode_values(values) <>
      <<page_size::32>>
  end

  defp encode_values(values) when values == [] or map_size(values) == 0 do
    <<>>
  end

  defp encode_values(values) when is_list(values) do
    for value <- values, into: <<length(values)::16>> do
      value = encode_query_value(value)
      <<byte_size(value)::32>> <> value
    end
  end

  defp encode_values(values) when is_map(values) do
    for {name, value} <- values, into: <<map_size(values)::16>> do
      name = to_string(name)
      value = encode_query_value(value)
      <<byte_size(name)::16>> <> name <>
        <<byte_size(value)::32>> <> value
    end
  end

  defp encode_query_value(string) when is_binary(string) do
    string
  end

  defp encode_query_value(int) when is_integer(int) do
    <<int::32>>
  end

  # ERROR

  def decode_response(header, body, query \\ nil)

  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x00, _::32>>, body, _query) do
    <<code::32-signed>> <> rest = body
    {message, ""} = decode_string(rest)
    Error.new(code, message)
  end

  # READY
  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x02, _::32>>, <<>>, nil) do
    :ok
  end

  # SUPPORTED
  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x06, _::32>>, body, nil) do
    {content, ""} = decode_string_multimap(body)
    content
  end

  # RESULT
  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x08, _::32>>, body, %Query{} = query) do
    decode_result_response(body, query)
  end

  # Void
  defp decode_result_response(<<0x0001::32-signed>>, _query) do
    :ok
  end

  # Rows
  defp decode_result_response(<<0x0002::32-signed>> <> rest, query) do
    result = case query.prepared do
      {_query_id, result} -> result
      nil -> %Result{}
    end
    {result, rest} = decode_metadata(result, rest)
    rows = decode_rows(rest, result.column_specs)
    %{result | rows: rows}
  end

  # Set keyspace
  defp decode_result_response(<<0x0003::32-signed>> <> rest, _query) do
    {keyspace, ""} = decode_string(rest)
    keyspace
  end

  # Prepared
  defp decode_result_response(<<0x0004::32-signed>> <> rest, query) do
    <<byte_count::16, query_id::bytes-size(byte_count)>> <> rest = rest
    {_result, rest} = decode_metadata(%Result{}, rest)
    {result, <<>>} = decode_metadata(%Result{}, rest)
    %{query | prepared: {query_id, result}}
  end

  defp decode_metadata(result, <<flags::4-bytes, column_count::32-signed>> <> rest) do
    <<_::29, no_metadata::1, has_more_pages::1, global_table_spec::1>> = flags

    {_result, rest} = decode_paging_state(result, has_more_pages, rest)
    cond do
      no_metadata == 1 ->
        {result, rest}
      global_table_spec == 1 ->
        {keyspace_name, rest} = decode_string(rest)
        {table_name, rest} = decode_string(rest)
        {column_specs, rest} = decode_column_specs(rest, column_count, {keyspace_name, table_name}, [])
        {%{result | column_specs: column_specs}, rest}
      true ->
        {column_specs, rest} = decode_column_specs(rest, column_count, nil, [])
        {%{result | column_specs: column_specs}, rest}
    end
  end

  defp decode_paging_state(result, 0, buffer) do
    {result, buffer}
  end

  defp decode_paging_state(result, 1, buffer) do
    <<byte_count::32, paging_state::bytes-size(byte_count)>> <> rest = buffer
    {%{result | paging_state: paging_state}, rest}
  end

  defp decode_rows(<<row_count::32-signed>> <> buffer, column_specs) do
    {rows, ""} = decode_rows(row_count, buffer, column_specs, column_specs, [%{}])
    rows
  end

  def decode_rows(0, buffer, column_specs, column_specs, [_ | acc]) do
    {Enum.reverse(acc), buffer}
  end

  def decode_rows(row_count, buffer, column_specs, [], acc) do
    decode_rows(row_count - 1, buffer, column_specs, column_specs, [%{} | acc])
  end

  def decode_rows(row_count, <<size::32-signed>> <> buffer, column_specs, [{_, _, name, type} | rest], [row | acc]) do
    {value, buffer} = decode_value(size, buffer, type)
    row = Map.put(row, name, value)
    decode_rows(row_count, buffer, column_specs, rest, [row | acc])
  end

  defp decode_value(<<size::32-signed>> <> buffer, type) do
    decode_value(size, buffer, type)
  end

  defp decode_value(value_size, buffer, :ascii) do
    <<value::size(value_size), buffer::bytes>> = buffer
    {value, buffer}
  end

  defp decode_value(8, <<value::64-signed>> <> buffer, :bigint) do
    {value, buffer}
  end

  defp decode_value(1, <<value::8>> <> buffer, :boolean) do
    {value == 1, buffer}
  end

  # TODO: Decimal

  defp decode_value(8, <<value::64-float>> <> buffer, :double) do
    {value, buffer}
  end

  defp decode_value(4, <<value::32-float>> <> buffer, :float) do
    {value, buffer}
  end

  defp decode_value(4, <<address::4-bytes>> <> buffer, :inet) do
    <<n1, n2, n3, n4>> = address
    {{n1, n2, n3, n4}, buffer}
  end

  defp decode_value(16, <<address::16-bytes>> <> buffer, :inet) do
    <<n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16>> = address
    {{n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16}, buffer}
  end

  defp decode_value(4, <<value::32-signed>> <> buffer, :int) do
    {value, buffer}
  end

  defp decode_value(length, buffer, {:list, type}) do
    decode_list(length, buffer, type, [])
  end

  defp decode_value(size, buffer, {:map, key_type, value_type}) do
    decode_map(size, buffer, key_type, value_type, [])
  end

  defp decode_value(length, buffer, {:set, type}) do
    {list, buffer} = decode_list(length, buffer, type, [])
    {MapSet.new(list), buffer}
  end

  defp decode_value(length, buffer, :varchar) do
     <<text::size(length)-bytes>> <> buffer = buffer
    {text, buffer}
  end

  defp decode_value(8, <<value::64-signed>> <> buffer, :timestamp) do
    {value, buffer}
  end

  defp decode_list(0, buffer, _type, acc) do
    {Enum.reverse(acc), buffer}
  end

  defp decode_list(length, buffer, type, acc) do
    {elem, buffer} = decode_value(buffer, type)
    decode_list(length - 1, buffer, type, [elem | acc])
  end

  defp decode_map(0, buffer, _key_type, _value_type, acc) do
    {Map.new(acc), buffer}
  end

  defp decode_map(size, buffer, key_type, value_type, acc) do
    {key, buffer} = decode_value(buffer, key_type)
    {value, buffer} = decode_value(buffer, value_type)
    decode_map(size - 1, buffer, key_type, value_type, [{key, value} | acc])
  end

  defp decode_column_specs(rest, 0, _table_spec, acc) do
    {Enum.reverse(acc), rest}
  end

  defp decode_column_specs(rest, column_count, nil, acc) do
    {keyspace_name, rest} = decode_string(rest)
    {table_name, rest} = decode_string(rest)
    {name, rest} = decode_string(rest)
    {type, rest} = decode_type(rest)
    entry = {keyspace_name, table_name, name, type}
    decode_column_specs(rest, column_count - 1, nil, [entry | acc])
  end

  defp decode_column_specs(rest, column_count, table_spec, acc) do
    {keyspace_name, table_name} = table_spec
    {name, rest} = decode_string(rest)
    {type, rest} = decode_type(rest)
    entry = {keyspace_name, table_name, name, type}
    decode_column_specs(rest, column_count - 1, table_spec, [entry | acc])
  end

  defp decode_type(<<0x0000::16>> <> rest) do
    {name, rest} = decode_string(rest)
    {{:custom, name}, rest}
  end

  defp decode_type(<<0x0001::16>> <> rest) do
    {:ascii, rest}
  end

  defp decode_type(<<0x0002::16>> <> rest) do
    {:bigint, rest}
  end

  defp decode_type(<<0x0003::16>> <> rest) do
    {:blob, rest}
  end

  defp decode_type(<<0x0004::16>> <> rest) do
    {:boolean, rest}
  end

  defp decode_type(<<0x0005::16>> <> rest) do
    {:counter, rest}
  end

  defp decode_type(<<0x0006::16>> <> rest) do
    {:decimal, rest}
  end

  defp decode_type(<<0x0007::16>> <> rest) do
    {:double, rest}
  end

  defp decode_type(<<0x0008::16>> <> rest) do
    {:float, rest}
  end

  defp decode_type(<<0x0009::16>> <> rest) do
    {:int, rest}
  end

  defp decode_type(<<0x000B::16>> <> rest) do
    {:timestamp, rest}
  end

  defp decode_type(<<0x000C::16>> <> rest) do
    {:uuid, rest}
  end

  defp decode_type(<<0x000D::16>> <> rest) do
    {:varchar, rest}
  end

  defp decode_type(<<0x000E::16>> <> rest) do
    {:varint, rest}
  end

  defp decode_type(<<0x000F::16>> <> rest) do
    {:timeuuid, rest}
  end

  defp decode_type(<<0x0010::16>> <> rest) do
    {:inet, rest}
  end

  defp decode_type(<<0x0020::16>> <> rest) do
    {type, rest} = decode_type(rest)
    {{:list, type}, rest}
  end

  defp decode_type(<<0x0021::16>> <> rest) do
    {key_type, rest} = decode_type(rest)
    {value_type, rest} = decode_type(rest)
    {{:map, key_type, value_type}, rest}
  end

  defp decode_type(<<0x0022::16>> <> rest) do
    {type, rest} = decode_type(rest)
    {{:set, type}, rest}
  end

  # TODO: UDT

  defp decode_string_multimap(<<size::16>> <> rest) do
    decode_string_multimap(rest, size, %{})
  end

  defp decode_string_multimap(rest, 0, acc) do
    {acc, rest}
  end

  defp decode_string_multimap(rest, size, acc) do
    {key, rest} = decode_string(rest)
    {value, rest} = decode_string_list(rest)
    decode_string_multimap(rest, size - 1, Map.put(acc, key, value))
  end

  defp decode_string(<<size::16, content::size(size)-bytes>> <> rest) do
    {content, rest}
  end

  defp decode_string_list(<<size::16>> <> rest) do
    decode_string_list(rest, size, [])
  end

  defp decode_string_list(rest, 0, acc) do
    {Enum.reverse(acc), rest}
  end

  defp decode_string_list(rest, size, acc) do
    {elem, rest} = decode_string(rest)
    decode_string_list(rest, size - 1, [elem | acc])
  end
end

defmodule Xandra.Protocol do
  use Bitwise

  alias Xandra.{Query, Result, Error}

  def encode_query(statement, values, opts) do
    <<byte_size(statement)::32>> <> statement <>
      encode_params(values, opts)
  end

  def encode_prepared_query(id, values, opts) do
    <<byte_size(id)::16>> <> id <>
      encode_params(values, opts)
  end

  @consistency_levels %{
    0x0000 => :any,
    0x0001 => :one,
    0x0002 => :two,
    0x0003 => :three,
    0x0004 => :quorum,
    0x0005 => :all,
    0x0006 => :local_quorum,
    0x0007 => :each_quorum,
    0x0008 => :serial,
    0x0009 => :local_serial,
    0x000A => :local_one,
  }

  for {spec, level} <- @consistency_levels do
    defp encode_consistency_level(unquote(level)) do
      <<unquote(spec)::16>>
    end
  end

  defp encode_query_flags(values) when values == [] or map_size(values) == 0 do
    <<0x00 ||| 0x04>>
  end

  defp encode_query_flags(values) when is_list(values) do
    <<0x01 ||| 0x04>>
  end

  defp encode_query_flags(values) when is_map(values) do
    <<0x01 ||| 0x04 ||| 0x40>>
  end

  defp encode_params(values, opts) do
    consistency = Keyword.get(opts, :consistency, :one)
    page_size = Keyword.get(opts, :page_size, 10_000)

    encode_consistency_level(consistency) <>
      encode_query_flags(values) <>
      encode_values(values) <>
      <<page_size::32>>
  end

  defp encode_values(values) when values == [] or map_size(values) == 0 do
    <<>>
  end

  defp encode_values(values) when is_list(values) do
    for value <- values, into: <<length(values)::16>> do
      value = encode_query_value(value)
      <<byte_size(value)::32>> <> value
    end
  end

  defp encode_values(values) when is_map(values) do
    for {name, value} <- values, into: <<map_size(values)::16>> do
      name = to_string(name)
      value = encode_query_value(value)
      <<byte_size(name)::16>> <> name <>
        <<byte_size(value)::32>> <> value
    end
  end

  defp encode_query_value(string) when is_binary(string) do
    string
  end

  defp encode_query_value(int) when is_integer(int) do
    <<int::32>>
  end

  # ERROR

  def decode_response(header, body, query \\ nil)

  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x00, _::32>>, body, _query) do
    <<code::32-signed>> <> rest = body
    {message, ""} = decode_string(rest)
    Error.new(code, message)
  end

  # READY
  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x02, _::32>>, <<>>, nil) do
    :ok
  end

  # SUPPORTED
  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x06, _::32>>, body, nil) do
    {content, ""} = decode_string_multimap(body)
    content
  end

  # RESULT
  def decode_response(<<_cql_version, _flags, _stream_id::16, 0x08, _::32>>, body, %Query{} = query) do
    decode_result_response(body, query)
  end

  # Void
  defp decode_result_response(<<0x0001::32-signed>>, _query) do
    :ok
  end

  # Rows
  defp decode_result_response(<<0x0002::32-signed>> <> rest, query) do
    result = case query.prepared do
      {_query_id, result} -> result
      nil -> %Result{}
    end
    {result, rest} = decode_metadata(result, rest)
    rows = decode_rows(rest, result.column_specs)
    %{result | rows: rows}
  end

  # Set keyspace
  defp decode_result_response(<<0x0003::32-signed>> <> rest, _query) do
    {keyspace, ""} = decode_string(rest)
    keyspace
  end

  # Prepared
  defp decode_result_response(<<0x0004::32-signed>> <> rest, query) do
    <<byte_count::16, query_id::bytes-size(byte_count)>> <> rest = rest
    {_result, rest} = decode_metadata(%Result{}, rest)
    {result, <<>>} = decode_metadata(%Result{}, rest)
    %{query | prepared: {query_id, result}}
  end

  defp decode_metadata(result, <<flags::4-bytes, column_count::32-signed>> <> rest) do
    <<_::29, no_metadata::1, has_more_pages::1, global_table_spec::1>> = flags

    {_result, rest} = decode_paging_state(result, has_more_pages, rest)
    cond do
      no_metadata == 1 ->
        {result, rest}
      global_table_spec == 1 ->
        {keyspace_name, rest} = decode_string(rest)
        {table_name, rest} = decode_string(rest)
        {column_specs, rest} = decode_column_specs(rest, column_count, {keyspace_name, table_name}, [])
        {%{result | column_specs: column_specs}, rest}
      true ->
        {column_specs, rest} = decode_column_specs(rest, column_count, nil, [])
        {%{result | column_specs: column_specs}, rest}
    end
  end

  defp decode_paging_state(result, 0, buffer) do
    {result, buffer}
  end

  defp decode_paging_state(result, 1, buffer) do
    <<byte_count::32, paging_state::bytes-size(byte_count)>> <> rest = buffer
    {%{result | paging_state: paging_state}, rest}
  end

  defp decode_rows(<<row_count::32-signed>> <> buffer, column_specs) do
    {rows, ""} = decode_rows(row_count, buffer, column_specs, column_specs, [%{}])
    rows
  end

  def decode_rows(0, buffer, column_specs, column_specs, [_ | acc]) do
    {Enum.reverse(acc), buffer}
  end

  def decode_rows(row_count, buffer, column_specs, [], acc) do
    decode_rows(row_count - 1, buffer, column_specs, column_specs, [%{} | acc])
  end

  def decode_rows(row_count, <<size::32-signed>> <> buffer, column_specs, [{_, _, name, type} | rest], [row | acc]) do
    {value, buffer} = decode_value(size, buffer, type)
    row = Map.put(row, name, value)
    decode_rows(row_count, buffer, column_specs, rest, [row | acc])
  end

  defp decode_value(<<size::32-signed>> <> buffer, type) do
    decode_value(size, buffer, type)
  end

  defp decode_value(value_size, buffer, :ascii) do
    <<value::size(value_size), buffer::bytes>> = buffer
    {value, buffer}
  end

  defp decode_value(8, <<value::64-signed>> <> buffer, :bigint) do
    {value, buffer}
  end

  defp decode_value(1, <<value::8>> <> buffer, :boolean) do
    {value == 1, buffer}
  end

  # TODO: Decimal

  defp decode_value(8, <<value::64-float>> <> buffer, :double) do
    {value, buffer}
  end

  defp decode_value(4, <<value::32-float>> <> buffer, :float) do
    {value, buffer}
  end

  defp decode_value(4, <<address::4-bytes>> <> buffer, :inet) do
    <<n1, n2, n3, n4>> = address
    {{n1, n2, n3, n4}, buffer}
  end

  defp decode_value(16, <<address::16-bytes>> <> buffer, :inet) do
    <<n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16>> = address
    {{n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11, n12, n13, n14, n15, n16}, buffer}
  end

  defp decode_value(4, <<value::32-signed>> <> buffer, :int) do
    {value, buffer}
  end

  defp decode_value(length, buffer, {:list, type}) do
    decode_list(length, buffer, type, [])
  end

  defp decode_value(size, buffer, {:map, key_type, value_type}) do
    decode_map(size, buffer, key_type, value_type, [])
  end

  defp decode_value(length, buffer, {:set, type}) do
    {list, buffer} = decode_list(length, buffer, type, [])
    {MapSet.new(list), buffer}
  end

  defp decode_value(length, buffer, :varchar) do
     <<text::size(length)-bytes>> <> buffer = buffer
    {text, buffer}
  end

  defp decode_value(8, <<value::64-signed>> <> buffer, :timestamp) do
    {value, buffer}
  end

  defp decode_list(0, buffer, _type, acc) do
    {Enum.reverse(acc), buffer}
  end

  defp decode_list(length, buffer, type, acc) do
    {elem, buffer} = decode_value(buffer, type)
    decode_list(length - 1, buffer, type, [elem | acc])
  end

  defp decode_map(0, buffer, _key_type, _value_type, acc) do
    {Map.new(acc), buffer}
  end

  defp decode_map(size, buffer, key_type, value_type, acc) do
    {key, buffer} = decode_value(buffer, key_type)
    {value, buffer} = decode_value(buffer, value_type)
    decode_map(size - 1, buffer, key_type, value_type, [{key, value} | acc])
  end

  defp decode_column_specs(rest, 0, _global_tables_spec, acc) do
    {Enum.reverse(acc), rest}
  end

  defp decode_column_specs(rest, column_count, nil, acc) do
    {keyspace_name, rest} = decode_string(rest)
    {table_name, rest} = decode_string(rest)
    {name, rest} = decode_string(rest)
    {type, rest} = decode_type(rest)
    entry = {keyspace_name, table_name, name, type}
    decode_column_specs(rest, column_count - 1, nil, [entry | acc])
  end

  defp decode_column_specs(rest, column_count, global_tables_spec, acc) do
    {keyspace_name, table_name} = global_tables_spec
    {name, rest} = decode_string(rest)
    {type, rest} = decode_type(rest)
    entry = {keyspace_name, table_name, name, type}
    decode_column_specs(rest, column_count - 1, global_tables_spec, [entry | acc])
  end

  defp decode_type(<<0x0000::16>> <> rest) do
    {name, rest} = decode_string(rest)
    {{:custom, name}, rest}
  end

  defp decode_type(<<0x0001::16>> <> rest) do
    {:ascii, rest}
  end

  defp decode_type(<<0x0002::16>> <> rest) do
    {:bigint, rest}
  end

  defp decode_type(<<0x0003::16>> <> rest) do
    {:blob, rest}
  end

  defp decode_type(<<0x0004::16>> <> rest) do
    {:boolean, rest}
  end

  defp decode_type(<<0x0005::16>> <> rest) do
    {:counter, rest}
  end

  defp decode_type(<<0x0006::16>> <> rest) do
    {:decimal, rest}
  end

  defp decode_type(<<0x0007::16>> <> rest) do
    {:double, rest}
  end

  defp decode_type(<<0x0008::16>> <> rest) do
    {:float, rest}
  end

  defp decode_type(<<0x0009::16>> <> rest) do
    {:int, rest}
  end

  defp decode_type(<<0x000B::16>> <> rest) do
    {:timestamp, rest}
  end

  defp decode_type(<<0x000C::16>> <> rest) do
    {:uuid, rest}
  end

  defp decode_type(<<0x000D::16>> <> rest) do
    {:varchar, rest}
  end

  defp decode_type(<<0x000E::16>> <> rest) do
    {:varint, rest}
  end

  defp decode_type(<<0x000F::16>> <> rest) do
    {:timeuuid, rest}
  end

  defp decode_type(<<0x0010::16>> <> rest) do
    {:inet, rest}
  end

  defp decode_type(<<0x0020::16>> <> rest) do
    {type, rest} = decode_type(rest)
    {{:list, type}, rest}
  end

  defp decode_type(<<0x0021::16>> <> rest) do
    {key_type, rest} = decode_type(rest)
    {value_type, rest} = decode_type(rest)
    {{:map, key_type, value_type}, rest}
  end

  defp decode_type(<<0x0022::16>> <> rest) do
    {type, rest} = decode_type(rest)
    {{:set, type}, rest}
  end

  # TODO: UDT

  defp decode_string_multimap(<<size::16>> <> rest) do
    decode_string_multimap(rest, size, %{})
  end

  defp decode_string_multimap(rest, 0, acc) do
    {acc, rest}
  end

  defp decode_string_multimap(rest, size, acc) do
    {key, rest} = decode_string(rest)
    {value, rest} = decode_string_list(rest)
    decode_string_multimap(rest, size - 1, Map.put(acc, key, value))
  end

  defp decode_string(<<size::16, content::size(size)-bytes>> <> rest) do
    {content, rest}
  end

  defp decode_string_list(<<size::16>> <> rest) do
    decode_string_list(rest, size, [])
  end

  defp decode_string_list(rest, 0, acc) do
    {Enum.reverse(acc), rest}
  end

  defp decode_string_list(rest, size, acc) do
    {elem, rest} = decode_string(rest)
    decode_string_list(rest, size - 1, [elem | acc])
  end
end


Update benchmark

Update benchmark

defmodule Bench do
  defmodule GitLoadSave do
    @file_name "benchee.run"

    def config(branch \\ "master") do
      case load(branch) do
        nil -> [save: save()]
        load -> [load: load, save: save()]
      end
    end

    defp load(branch \\ "master") do
      case branch == branch() do
        true ->
          nil

        false ->
          System.tmp_dir!()
          |> Path.join(app())
          |> Path.join(branch)
          |> Path.join(@file_name)
      end
    end

    defp save do
      tag = branch()

      path =
        System.tmp_dir!()
        |> Path.join(app())
        |> Path.join(tag)
        |> Path.join(@file_name)

      [path: path, tag: tag]
    end

    defp app, do: to_string(Mix.Project.config()[:app])

    defp branch,
      do:
        "git"
        |> System.cmd(["rev-parse", "--abbrev-ref", "HEAD"])
        |> trim()

    defp trim({str, 0}), do: String.trim(str)
  end

  def fun do
    schema =
      Xema.new(
        {:map,
         definitions: %{
           neg: {:integer, maximum: 0},
           pos: {:integer, minimum: 0}
         },
         properties: %{
           neg: {:ref, "#/definitions/neg"},
           pos: {:ref, "#/definitions/pos"}
         }}
      )

    data = %{neg: -5, pos: 6}

    fn -> true = Xema.valid?(schema, data) end
  end

  def run do
    config =
      [
        parallel: 4,
        print: [fast_warning: false],
        formatters: [
          Benchee.Formatters.Console
        ]
      ] ++ GitLoadSave.config()

    Benchee.run(
      %{"ref" => fun()},
      config
    )
  end
end

Bench.run()

defmodule Bench do
  defmodule GitStrategy do
    @file_name "benchee.run"

    defp app, do: to_string(Mix.Project.config()[:app])

    defp branch,
      do:
        "git"
        |> System.cmd(["rev-parse", "--abbrev-ref", "HEAD"])
        |> trim()

    defp trim({str, 0}), do: String.trim(str)

    def load(branch \\ "master") do
      case branch == branch() do
        true ->
          nil

        false ->
          System.tmp_dir!()
          |> Path.join(app())
          |> Path.join(branch)
          |> Path.join(@file_name)
      end
    end

    def save do
      tag = branch()

      path =
        System.tmp_dir!()
        |> Path.join(app())
        |> Path.join(tag)
        |> Path.join(@file_name)

      [path: path, tag: tag]
    end

    def config do
      case load() do
        nil -> [save: save()]
        load -> [load: load, save: save()]
      end
    end
  end

  def fun do
    schema =
      Xema.new(
        {:map,
         definitions: %{
           neg: {:integer, maximum: 0},
           pos: {:integer, minimum: 0}
         },
         properties: %{
           neg: {:ref, "#/definitions/neg"},
           pos: {:ref, "#/definitions/pos"}
         }}
      )

    data = %{neg: -5, pos: 6}

    fn -> true = Xema.valid?(schema, data) end
  end

  def run do
    config =
      [
        parallel: 4,
        print: [fast_warning: false],
        formatters: [
          Benchee.Formatters.Console
        ]
      ] ++ GitStrategy.config()

    Benchee.run(
      %{"ref" => fun()},
      config
    )
  end
end

Bench.run()


Remove the restriction on PBKDF2 size

Remove the restriction on PBKDF2 size

if Code.ensure_loaded?(:pbkdf2) do
  defmodule Cloak.Fields.PBKDF2 do
    @moduledoc """
    A custom `Ecto.Type` for deriving a key for fields using
    [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2).

    PBKDF2 is **more secure** than `Cloak.Fields.HMAC` and
    `Cloak.Fields.SHA256` because it uses [key
    stretching](https://en.wikipedia.org/wiki/Key_stretching) to increase the
    amount of time to compute hashes. This slows down brute-force attacks.

    ## Why

    If you store a hash of a field's value, you can then query on it as a proxy
    for an encrypted field. This works because PBKDF2 is deterministic and
    always results in the same value, while secure encryption does not. Be
    warned, however, that hashing will expose which fields have the same value,
    because they will contain the same hash.

    ## Configuration

    Create a `PBKDF2` field in your project:

    defmodule MyApp.Hashed.PBKDF2 do
    use Cloak.Fields.PBKDF2, otp_app: :my_app
    end

    Then, configure it with a `:secret`, an `:algorithm`, the maximum `:size`
    of the stored key (in bytes), and a number of `:iterations`, either using
    mix configuration:

    config :my_app, MyApp.Hashed.PBKDF2,
    algorithm: :sha256,
    iterations: 10_000,
    secret: "secret"

    Or using the `init/1` callback to fetch configuration at runtime:

    defmodule MyApp.Hashed.PBKDF2 do
    use Cloak.Fields.PBKDF2, otp_app: :my_app

    @impl Cloak.Fields.PBKDF2
    def init(config) do
    config = Keyword.merge(config, [
    algorithm: :sha256,
    iterations: 10_000,
    secret: System.get_env("PBKDF2_SECRET")
    ])

    {:ok, config}
    end
    end

    ## Usage

    Create the hash field with the type `:binary`. Add it to your schema
    definition like this:

    schema "table" do
    field :field_name, MyApp.Encrypted.Binary
    field :field_name_hash, MyApp.Hashed.PBKDF2
    end

    Ensure that the hash is updated whenever the target field changes with the
    `put_change/3` function:

    def changeset(struct, attrs \\\\ %{}) do
    struct
    |> cast(attrs, [:field_name, :field_name_hash])
    |> put_hashed_fields()
    end

    defp put_hashed_fields(changeset) do
    changeset
    |> put_change(:field_name_hash, get_field(changeset, :field_name))
    end

    Query the Repo using the `:field_name_hash` in any place you would typically
    query by `:field_name`.

    user = Repo.get_by(User, email_hash: "user@email.com")
    """

    @typedoc "Digest algorithms supported by Cloak.Field.PBKDF2"
    @type algorithms :: :md4 | :md5 | :ripemd160 | :sha | :sha224 | :sha256 | :sha384 | :sha512

    @doc """
    Configures the `PBKDF2` field using runtime information.

    ## Example

    @impl Cloak.Fields.PBKDF2
    def init(config) do
    config = Keyword.merge(config, [
    algorithm: :sha256,
    secret: System.get_env("PBKDF2_SECRET")
    ])

    {:ok, config}
    end
    """
    @callback init(config :: Keyword.t()) :: {:ok, Keyword.t()} | {:error, any}

    @doc false
    defmacro __using__(opts) do
      otp_app = Keyword.fetch!(opts, :otp_app)

      quote do
        @behaviour Cloak.Fields.PBKDF2
        @behaviour Ecto.Type
        @algorithms ~w[
          md4
          md5
          ripemd160
          sha
          sha224
          sha256
          sha384
          sha512
        ]a

        @impl Cloak.Fields.PBKDF2
        def init(config) do
          defaults = [algorithm: :sha256, iterations: 10_000, size: 32]

          {:ok, defaults |> Keyword.merge(config)}
        end

        @impl Ecto.Type
        def type, do: :binary

        @impl Ecto.Type
        def cast(nil), do: {:ok, nil}
        def cast(value) when is_binary(value), do: {:ok, value}
        def cast(_value), do: :error

        @impl Ecto.Type
        def dump(nil), do: {:ok, nil}

        def dump(value) when is_binary(value) do
          config = build_config()
          :pbkdf2.pbkdf2({:hmac, config[:algorithm]}, value, config[:secret], config[:size])
        end

        def dump(_value), do: :error

        @impl Ecto.Type
        def load(value), do: {:ok, value}

        defoverridable init: 1, type: 0, cast: 1, dump: 1, load: 1

        defp build_config do
          {:ok, config} =
            unquote(otp_app)
            |> Application.get_env(__MODULE__, [])
            |> init()

          validate_config(config)
        end

        defp validate_config(config) do
          unless is_binary(config[:secret]) do
            secret = inspect(config[:secret])

            raise Cloak.InvalidConfig, "#{secret} is an invalid secret for #{inspect(__MODULE__)}"
          end

          unless config[:algorithm] in @algorithms do
            algo = inspect(config[:algorithm])

            raise Cloak.InvalidConfig,
                  "#{algo} is an invalid hash algorithm for #{inspect(__MODULE__)}"
          end

          unless is_integer(config[:iterations]) && config[:iterations] > 0 do
            iterations = inspect(config[:iterations])

            raise Cloak.InvalidConfig,
                  "#{iterations} must be a positive integer for #{inspect(__MODULE__)}"
          end

          unless is_integer(config[:size]) && config[:size] > 0 do
            size = inspect(config[:size])

            raise Cloak.InvalidConfig,
                  "#{size} should be a positive integer for #{inspect(__MODULE__)}"
          end

          config
        end
      end
    end
  end
end

if Code.ensure_loaded?(:pbkdf2) do
  defmodule Cloak.Fields.PBKDF2 do
    @moduledoc """
    A custom `Ecto.Type` for deriving a key for fields using
    [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2).

    PBKDF2 is **more secure** than `Cloak.Fields.HMAC` and
    `Cloak.Fields.SHA256` because it uses [key
    stretching](https://en.wikipedia.org/wiki/Key_stretching) to increase the
    amount of time to compute hashes. This slows down brute-force attacks.

    ## Why

    If you store a hash of a field's value, you can then query on it as a proxy
    for an encrypted field. This works because PBKDF2 is deterministic and
    always results in the same value, while secure encryption does not. Be
    warned, however, that hashing will expose which fields have the same value,
    because they will contain the same hash.

    ## Configuration

    Create a `PBKDF2` field in your project:

    defmodule MyApp.Hashed.PBKDF2 do
    use Cloak.Fields.PBKDF2, otp_app: :my_app
    end

    Then, configure it with a `:secret`, an `:algorithm`, the maximum `:size`
    of the stored key (in bytes), and a number of `:iterations`, either using
    mix configuration:

    config :my_app, MyApp.Hashed.PBKDF2,
    algorithm: :sha256,
    iterations: 10_000,
    secret: "secret"

    Or using the `init/1` callback to fetch configuration at runtime:

    defmodule MyApp.Hashed.PBKDF2 do
    use Cloak.Fields.PBKDF2, otp_app: :my_app

    @impl Cloak.Fields.PBKDF2
    def init(config) do
    config = Keyword.merge(config, [
    algorithm: :sha256,
    iterations: 10_000,
    secret: System.get_env("PBKDF2_SECRET")
    ])

    {:ok, config}
    end
    end

    ## Usage

    Create the hash field with the type `:binary`. Add it to your schema
    definition like this:

    schema "table" do
    field :field_name, MyApp.Encrypted.Binary
    field :field_name_hash, MyApp.Hashed.PBKDF2
    end

    Ensure that the hash is updated whenever the target field changes with the
    `put_change/3` function:

    def changeset(struct, attrs \\\\ %{}) do
    struct
    |> cast(attrs, [:field_name, :field_name_hash])
    |> put_hashed_fields()
    end

    defp put_hashed_fields(changeset) do
    changeset
    |> put_change(:field_name_hash, get_field(changeset, :field_name))
    end

    Query the Repo using the `:field_name_hash` in any place you would typically
    query by `:field_name`.

    user = Repo.get_by(User, email_hash: "user@email.com")
    """

    @typedoc "Digest algorithms supported by Cloak.Field.PBKDF2"
    @type algorithms :: :md4 | :md5 | :ripemd160 | :sha | :sha224 | :sha256 | :sha384 | :sha512

    @doc """
    Configures the `PBKDF2` field using runtime information.

    ## Example

    @impl Cloak.Fields.PBKDF2
    def init(config) do
    config = Keyword.merge(config, [
    algorithm: :sha256,
    secret: System.get_env("PBKDF2_SECRET")
    ])

    {:ok, config}
    end
    """
    @callback init(config :: Keyword.t()) :: {:ok, Keyword.t()} | {:error, any}

    @doc false
    defmacro __using__(opts) do
      otp_app = Keyword.fetch!(opts, :otp_app)

      quote do
        @behaviour Cloak.Fields.PBKDF2
        @behaviour Ecto.Type
        @algorithms ~w[
          md4
          md5
          ripemd160
          sha
          sha224
          sha256
          sha384
          sha512
        ]a
        @sizes [16, 32, 64, 128]

        @impl Cloak.Fields.PBKDF2
        def init(config) do
          defaults = [algorithm: :sha256, iterations: 10_000, size: 32]

          {:ok, defaults |> Keyword.merge(config)}
        end

        @impl Ecto.Type
        def type, do: :binary

        @impl Ecto.Type
        def cast(nil), do: {:ok, nil}
        def cast(value) when is_binary(value), do: {:ok, value}
        def cast(_value), do: :error

        @impl Ecto.Type
        def dump(nil), do: {:ok, nil}

        def dump(value) when is_binary(value) do
          config = build_config()
          :pbkdf2.pbkdf2({:hmac, config[:algorithm]}, value, config[:secret], config[:size])
        end

        def dump(_value), do: :error

        @impl Ecto.Type
        def load(value), do: {:ok, value}

        defoverridable init: 1, type: 0, cast: 1, dump: 1, load: 1

        defp build_config do
          {:ok, config} =
            unquote(otp_app)
            |> Application.get_env(__MODULE__, [])
            |> init()

          validate_config(config)
        end

        defp validate_config(config) do
          unless is_binary(config[:secret]) do
            secret = inspect(config[:secret])

            raise Cloak.InvalidConfig, "#{secret} is an invalid secret for #{inspect(__MODULE__)}"
          end

          unless config[:algorithm] in @algorithms do
            algo = inspect(config[:algorithm])

            raise Cloak.InvalidConfig,
                  "#{algo} is an invalid hash algorithm for #{inspect(__MODULE__)}"
          end

          unless is_integer(config[:iterations]) && config[:iterations] > 0 do
            iterations = inspect(config[:iterations])

            raise Cloak.InvalidConfig,
                  "#{iterations} must be a positive integer for #{inspect(__MODULE__)}"
          end

          unless config[:size] in @sizes do
            size = inspect(config[:size])

            raise Cloak.InvalidConfig,
              "#{size} should be one of 16, 32, 64, or 128 for #{inspect(__MODULE__)}"
          end

          config
        end
      end
    end
  end
end


Improve ExUnit documentation for the :test tag (#6271)

defmodule ExUnit do
  @moduledoc """
  Unit testing framework for Elixir.

  ## Example

  A basic setup for ExUnit is shown below:

      # File: assertion_test.exs

      # 1) Start ExUnit.
      ExUnit.start

      # 2) Create a new test module (test case) and use "ExUnit.Case".
      defmodule AssertionTest do
        # 3) Notice we pass "async: true", this runs the test case
        #    concurrently with other test cases. The individual tests
        #    within each test case are still run serially.
        use ExUnit.Case, async: true

        # 4) Use the "test" macro instead of "def" for clarity.
        test "the truth" do
          assert true
        end
      end

  To run the tests above, run the file using `elixir` from the
  command line. Assuming you named the file `assertion_test.exs`,
  you can run it as:

      elixir assertion_test.exs

  ## Case, Callbacks and Assertions

  See `ExUnit.Case` and `ExUnit.Callbacks` for more information
  about defining test cases and setting up callbacks.

  The `ExUnit.Assertions` module contains a set of macros to
  generate assertions with appropriate error messages.

  ## Integration with Mix

  Mix is the project management and build tool for Elixir. Invoking `mix test`
  from the command line will run the tests in each file matching the pattern
  `*_test.exs` found in the `test` directory of your project.

  You must create a `test_helper.exs` file inside the
  `test` directory and put the code common to all tests there.

  The minimum example of a `test_helper.exs` file would be:

      # test/test_helper.exs
      ExUnit.start

  Mix will load the `test_helper.exs` file before executing the tests.
  It is not necessary to `require` the `test_helper.exs` file in your test
  files. See `Mix.Tasks.Test` for more information.
  """

  @typedoc "The error state returned by ExUnit.Test and ExUnit.TestCase"
  @type state  :: nil | {:failed, failed} | {:skip, binary} | {:invalid, module}
  @type failed :: [{Exception.kind, reason :: term, stacktrace :: [tuple]}]

  defmodule Test do
    @moduledoc """
    A struct that keeps information about the test.

    It is received by formatters and contains the following fields:

      * `:name`  - the test name
      * `:case`  - the test case
      * `:state` - the test error state (see ExUnit.state)
      * `:time`  - the time to run the test
      * `:tags`  - the test tags
      * `:logs`  - the captured logs

    """
    defstruct [:name, :case, :state, time: 0, tags: %{}, logs: ""]

    @type t :: %__MODULE__{
                 name: atom,
                 case: module,
                 state: ExUnit.state,
                 time: non_neg_integer,
                 tags: map}
  end

  defmodule TestCase do
    @moduledoc """
    A struct that keeps information about the test case.

    It is received by formatters and contains the following fields:

      * `:name`  - the test case name
      * `:state` - the test error state (see ExUnit.state)
      * `:tests` - all tests for this case

    """
    defstruct [:name, :state, tests: []]

    @type t :: %__MODULE__{
                 name: module,
                 state: ExUnit.state,
                 tests: [ExUnit.Test.t]}
  end

  defmodule TimeoutError do
    defexception [:timeout, :type]

    def message(%{timeout: timeout, type: type}) do
      """
      #{type} timed out after #{timeout}ms. You can change the timeout:

        1. per test by setting "@tag timeout: x"
        2. per case by setting "@moduletag timeout: x"
        3. globally via "ExUnit.start(timeout: x)" configuration
        4. or set it to infinity per run by calling "mix test --trace"
           (useful when using IEx.pry)

      Timeouts are given as integers in milliseconds.
      """
    end
  end

  use Application

  @doc false
  def start(_type, []) do
    children = [
      ExUnit.Server,
      ExUnit.CaptureServer,
      ExUnit.OnExitHandler
    ]

    opts = [strategy: :one_for_one, name: ExUnit.Supervisor]
    Supervisor.start_link(children, opts)
  end

  @doc """
  Starts ExUnit and automatically runs tests right before the
  VM terminates.

  It accepts a set of options to configure `ExUnit`
  (the same ones accepted by `configure/1`).

  If you want to run tests manually, you can set the `:autorun` option
  to `false` and use `run/1` to run tests.
  """
  def start(options \\ []) do
    {:ok, _} = Application.ensure_all_started(:ex_unit)

    configure(options)
    config = put_defaults(configuration())

    if Application.fetch_env!(:ex_unit, :autorun) do
      Application.put_env(:ex_unit, :autorun, false)

      System.at_exit fn
        0 ->
          time = ExUnit.Server.cases_loaded()
          %{failures: failures} = ExUnit.Runner.run(config, time)
          System.at_exit fn _ ->
            if failures > 0, do: exit({:shutdown, 1})
          end
        _ ->
          :ok
      end
    end
  end

  @doc """
  Configures ExUnit.

  ## Options

  ExUnit supports the following options:

    * `:assert_receive_timeout` - the timeout to be used on `assert_receive`
      calls, defaults to `100` milliseconds;

    * `:autorun` - if ExUnit should run by default on exit. Defaults to `true`;

    * `:capture_log` - if ExUnit should default to keeping track of log messages
      and print them on test failure. Can be overridden for individual tests via
      `@tag capture_log: false`. Defaults to `false`;

    * `:case_load_timeout` - the timeout to be used when loading a test case,
      defaults to `60_000` milliseconds;

    * `:colors` - a keyword list of colors to be used by some formatters.
      The only option so far is `[enabled: boolean]` which defaults to `IO.ANSI.enabled?/0`;

    * `:exclude` - specifies which tests are run by skipping tests that match the
      filter;

    * `:formatters` - the formatters that will print results,
      defaults to `[ExUnit.CLIFormatter]`;

    * `:include` - specifies which tests are run by skipping tests that do not
      match the filter. Keep in mind that all tests are included by default, so unless they are
      excluded first, the `:include` option has no effect. To only run the tests
      that match the `:include` filter, exclude the `:test` tag first (see the
      documentation for `ExUnit.Case` for more information on tags);

    * `:max_cases` - maximum number of cases to run in parallel.
      It defaults to `System.schedulers_online * 2` to
      optimize both CPU-bound and IO-bound tests;

    * `:refute_receive_timeout` - the timeout to be used on `refute_receive`
      calls, defaults to `100` milliseconds;

    * `:seed` - an integer seed value to randomize the test suite;

    * `:stacktrace_depth` - configures the stacktrace depth to be used
      on formatting and reporters, defaults to `20`;

    * `:timeout` - sets the timeout for the tests, defaults to `60_000` milliseconds;

    * `:trace` - sets ExUnit into trace mode, this sets `:max_cases` to `1` and
      prints each test case and test while running. Note that in trace mode test timeouts
      will be ignored.

  """
  def configure(options) do
    Enum.each options, fn {k, v} ->
      Application.put_env(:ex_unit, k, v)
    end
  end

  @doc """
  Returns ExUnit configuration.
  """
  def configuration do
    Application.get_all_env(:ex_unit)
  end

  @doc """
  Returns the pluralization for `word`.

  If one is not registered, returns the word appended with an "s".
  """
  @spec plural_rule(binary) :: binary
  def plural_rule(word) when is_binary(word) do
    Application.get_env(:ex_unit, :plural_rules, %{})
    |> Map.get(word, "#{word}s")
  end

  @doc """
  Registers a `pluralization` for `word`.

  If one is already registered, it is replaced.
  """
  @spec plural_rule(binary, binary) :: :ok
  def plural_rule(word, pluralization) when is_binary(word) and is_binary(pluralization) do
    plural_rules =
      Application.get_env(:ex_unit, :plural_rules, %{})
      |> Map.put(word, pluralization)
    configure(plural_rules: plural_rules)
  end

  @doc """
  Runs the tests. It is invoked automatically
  if ExUnit is started via `start/1`.

  Returns a map containing the total number of tests, the number
  of failures and the number of skipped tests.
  """
  def run do
    config = put_defaults(configuration())
    ExUnit.Runner.run(config, nil)
  end

  defp put_defaults(opts) do
    opts
    |> put_seed()
    |> put_max_cases()
  end

  defp put_seed(opts) do
    Keyword.put_new_lazy(opts, :seed, fn ->
      seed = :os.timestamp |> elem(2)
      Application.put_env(:ex_unit, :seed, seed)
      seed
    end)
  end

  defp put_max_cases(opts) do
    max_cases = max_cases(opts)
    Application.put_env(:ex_unit, :max_cases, max_cases)
    Keyword.put(opts, :max_cases, max_cases)
  end

  defp max_cases(opts) do
    cond do
      opts[:trace] -> 1
      max = opts[:max_cases] -> max
      true -> System.schedulers_online * 2
    end
  end
end

defmodule ExUnit do
  @moduledoc """
  Unit testing framework for Elixir.

  ## Example

  A basic setup for ExUnit is shown below:

      # File: assertion_test.exs

      # 1) Start ExUnit.
      ExUnit.start

      # 2) Create a new test module (test case) and use "ExUnit.Case".
      defmodule AssertionTest do
        # 3) Notice we pass "async: true", this runs the test case
        #    concurrently with other test cases. The individual tests
        #    within each test case are still run serially.
        use ExUnit.Case, async: true

        # 4) Use the "test" macro instead of "def" for clarity.
        test "the truth" do
          assert true
        end
      end

  To run the tests above, run the file using `elixir` from the
  command line. Assuming you named the file `assertion_test.exs`,
  you can run it as:

      elixir assertion_test.exs

  ## Case, Callbacks and Assertions

  See `ExUnit.Case` and `ExUnit.Callbacks` for more information
  about defining test cases and setting up callbacks.

  The `ExUnit.Assertions` module contains a set of macros to
  generate assertions with appropriate error messages.

  ## Integration with Mix

  Mix is the project management and build tool for Elixir. Invoking `mix test`
  from the command line will run the tests in each file matching the pattern
  `*_test.exs` found in the `test` directory of your project.

  You must create a `test_helper.exs` file inside the
  `test` directory and put the code common to all tests there.

  The minimum example of a `test_helper.exs` file would be:

      # test/test_helper.exs
      ExUnit.start

  Mix will load the `test_helper.exs` file before executing the tests.
  It is not necessary to `require` the `test_helper.exs` file in your test
  files. See `Mix.Tasks.Test` for more information.
  """

  @typedoc "The error state returned by ExUnit.Test and ExUnit.TestCase"
  @type state  :: nil | {:failed, failed} | {:skip, binary} | {:invalid, module}
  @type failed :: [{Exception.kind, reason :: term, stacktrace :: [tuple]}]

  defmodule Test do
    @moduledoc """
    A struct that keeps information about the test.

    It is received by formatters and contains the following fields:

      * `:name`  - the test name
      * `:case`  - the test case
      * `:state` - the test error state (see ExUnit.state)
      * `:time`  - the time to run the test
      * `:tags`  - the test tags
      * `:logs`  - the captured logs

    """
    defstruct [:name, :case, :state, time: 0, tags: %{}, logs: ""]

    @type t :: %__MODULE__{
                 name: atom,
                 case: module,
                 state: ExUnit.state,
                 time: non_neg_integer,
                 tags: map}
  end

  defmodule TestCase do
    @moduledoc """
    A struct that keeps information about the test case.

    It is received by formatters and contains the following fields:

      * `:name`  - the test case name
      * `:state` - the test error state (see ExUnit.state)
      * `:tests` - all tests for this case

    """
    defstruct [:name, :state, tests: []]

    @type t :: %__MODULE__{
                 name: module,
                 state: ExUnit.state,
                 tests: [ExUnit.Test.t]}
  end

  defmodule TimeoutError do
    defexception [:timeout, :type]

    def message(%{timeout: timeout, type: type}) do
      """
      #{type} timed out after #{timeout}ms. You can change the timeout:

        1. per test by setting "@tag timeout: x"
        2. per case by setting "@moduletag timeout: x"
        3. globally via "ExUnit.start(timeout: x)" configuration
        4. or set it to infinity per run by calling "mix test --trace"
           (useful when using IEx.pry)

      Timeouts are given as integers in milliseconds.
      """
    end
  end

  use Application

  @doc false
  def start(_type, []) do
    children = [
      ExUnit.Server,
      ExUnit.CaptureServer,
      ExUnit.OnExitHandler
    ]

    opts = [strategy: :one_for_one, name: ExUnit.Supervisor]
    Supervisor.start_link(children, opts)
  end

  @doc """
  Starts ExUnit and automatically runs tests right before the
  VM terminates. It accepts a set of options to configure `ExUnit`
  (the same ones accepted by `configure/1`).

  If you want to run tests manually, you can set `:autorun` to `false`.
  """
  def start(options \\ []) do
    {:ok, _} = Application.ensure_all_started(:ex_unit)

    configure(options)
    config = put_defaults(configuration())

    if Application.fetch_env!(:ex_unit, :autorun) do
      Application.put_env(:ex_unit, :autorun, false)

      System.at_exit fn
        0 ->
          time = ExUnit.Server.cases_loaded()
          %{failures: failures} = ExUnit.Runner.run(config, time)
          System.at_exit fn _ ->
            if failures > 0, do: exit({:shutdown, 1})
          end
        _ ->
          :ok
      end
    end
  end

  @doc """
  Configures ExUnit.

  ## Options

  ExUnit supports the following options:

    * `:assert_receive_timeout` - the timeout to be used on `assert_receive`
      calls, defaults to `100` milliseconds;

    * `:autorun` - if ExUnit should run by default on exit. Defaults to `true`;

    * `:capture_log` - if ExUnit should default to keeping track of log messages
      and print them on test failure. Can be overridden for individual tests via
      `@tag capture_log: false`. Defaults to `false`;

    * `:case_load_timeout` - the timeout to be used when loading a test case,
      defaults to `60_000` milliseconds;

    * `:colors` - a keyword list of colors to be used by some formatters.
      The only option so far is `[enabled: boolean]` which defaults to `IO.ANSI.enabled?/0`;

    * `:exclude` - specifies which tests are run by skipping tests that match the
      filter;

    * `:formatters` - the formatters that will print results,
      defaults to `[ExUnit.CLIFormatter]`;

    * `:include` - specifies which tests are run by skipping tests that do not
      match the filter. Keep in mind that all tests are included by default, so unless they are
      excluded first, the `:include` option has no effect;

    * `:max_cases` - maximum number of cases to run in parallel.
      It defaults to `System.schedulers_online * 2` to
      optimize both CPU-bound and IO-bound tests;

    * `:refute_receive_timeout` - the timeout to be used on `refute_receive`
      calls, defaults to `100` milliseconds;

    * `:seed` - an integer seed value to randomize the test suite;

    * `:stacktrace_depth` - configures the stacktrace depth to be used
      on formatting and reporters, defaults to `20`;

    * `:timeout` - sets the timeout for the tests, defaults to `60_000` milliseconds;

    * `:trace` - sets ExUnit into trace mode, this sets `:max_cases` to `1` and
      prints each test case and test while running. Note that in trace mode test timeouts
      will be ignored.

  """
  def configure(options) do
    Enum.each options, fn {k, v} ->
      Application.put_env(:ex_unit, k, v)
    end
  end

  @doc """
  Returns ExUnit configuration.
  """
  def configuration do
    Application.get_all_env(:ex_unit)
  end

  @doc """
  Returns the pluralization for `word`.

  If one is not registered, returns the word appended with an "s".
  """
  @spec plural_rule(binary) :: binary
  def plural_rule(word) when is_binary(word) do
    Application.get_env(:ex_unit, :plural_rules, %{})
    |> Map.get(word, "#{word}s")
  end

  @doc """
  Registers a `pluralization` for `word`.

  If one is already registered, it is replaced.
  """
  @spec plural_rule(binary, binary) :: :ok
  def plural_rule(word, pluralization) when is_binary(word) and is_binary(pluralization) do
    plural_rules =
      Application.get_env(:ex_unit, :plural_rules, %{})
      |> Map.put(word, pluralization)
    configure(plural_rules: plural_rules)
  end

  @doc """
  API used to run the tests. It is invoked automatically
  if ExUnit is started via `ExUnit.start/1`.

  Returns a map containing the total number of tests, the number
  of failures and the number of skipped tests.
  """
  def run do
    config = put_defaults(configuration())
    ExUnit.Runner.run(config, nil)
  end

  defp put_defaults(opts) do
    opts
    |> put_seed()
    |> put_max_cases()
  end

  defp put_seed(opts) do
    Keyword.put_new_lazy(opts, :seed, fn ->
      seed = :os.timestamp |> elem(2)
      Application.put_env(:ex_unit, :seed, seed)
      seed
    end)
  end

  defp put_max_cases(opts) do
    max_cases = max_cases(opts)
    Application.put_env(:ex_unit, :max_cases, max_cases)
    Keyword.put(opts, :max_cases, max_cases)
  end

  defp max_cases(opts) do
    cond do
      opts[:trace] -> 1
      max = opts[:max_cases] -> max
      true -> System.schedulers_online * 2
    end
  end
end