Only add at_exit if failures are positive

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) Note that 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. Run `mix help test` for more information.
  """

  @typedoc """
  All tests start with a state of `nil`.

  A finished test can be in one of five states:

    1. Passed (also represented by `nil`)
    2. Failed
    3. Skipped (via @tag :skip)
    4. Excluded (via :exclude filters)
    5. Invalid (when setup_all fails)

  """
  @type state ::
          nil | {:excluded, binary} | {:failed, failed} | {:invalid, module} | {:skipped, binary}

  @typedoc "The error state returned by `ExUnit.Test` and `ExUnit.TestModule`"
  @type failed :: [{Exception.kind(), reason :: term, Exception.stacktrace()}]

  @typedoc "A map representing the results of running a test suite"
  @type suite_result :: %{
          excluded: non_neg_integer,
          failures: non_neg_integer,
          skipped: non_neg_integer,
          total: non_neg_integer
        }

  @type test_id :: {module, name :: atom}

  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
      * `:module` - the test module
      * `:state` - the finished test state (see `t:ExUnit.state/0`)
      * `:time` - the duration in microseconds of the test's runtime
      * `:tags` - the test tags
      * `:logs` - the captured logs

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

    # TODO: Remove the `:case` field on v2.0
    @type t :: %__MODULE__{
            name: atom,
            case: module,
            module: module,
            state: ExUnit.state(),
            time: non_neg_integer,
            tags: map,
            logs: String.t()
          }
  end

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

    It is received by formatters and contains the following fields:

      * `:file`  - (since v1.11.0) the file of the test module

      * `:name`  - the test module name

      * `:state` - the test error state (see `t:ExUnit.state/0`)

      * `:tests` - all tests in this module

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

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

  defmodule TestCase do
    # TODO: Remove this module on v2.0 (it has been replaced by TestModule)
    @moduledoc false
    defstruct [:name, :state, tests: []]

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

  defmodule TimeoutError do
    defexception [:timeout, :type]

    @impl true
    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" (accepts :infinity)
        2. per test module by setting "@moduletag timeout: x" (accepts :infinity)
        3. globally via "ExUnit.start(timeout: x)" configuration
        4. by running "mix test --timeout x" which sets timeout
        5. or by running "mix test --trace" which sets timeout to infinity
           (useful when using IEx.pry/0)

      where "x" is the timeout given as integer in milliseconds (defaults to 60_000).
      """
    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/0` to run tests.
  """
  @spec start(Keyword.t()) :: :ok
  def start(options \\ []) do
    {:ok, _} = Application.ensure_all_started(:ex_unit)

    configure(options)

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

      System.at_exit(fn
        0 ->
          time = ExUnit.Server.modules_loaded()
          options = persist_defaults(configuration())
          %{failures: failures} = ExUnit.Runner.run(options, time)

          if failures > 0 do
            System.at_exit(fn _ -> exit({:shutdown, 1}) end)
          end

        _ ->
          :ok
      end)
    else
      :ok
    end
  end

  @doc """
  Configures ExUnit.

  ## Options

  ExUnit supports the following options:

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

    * `: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`;

    * `:colors` - a keyword list of color options to be used by some formatters:
      * `:enabled` - boolean option to enable colors, defaults to `IO.ANSI.enabled?/0`;
      * `:diff_insert` - color of the insertions on diffs, defaults to `:green`;
      * `:diff_insert_whitespace` - color of the whitespace insertions on diffs,
        defaults to `IO.ANSI.color_background(2, 0, 0)`;
      * `:diff_delete` - color of the deletions on diffs, defaults to `:red`;
      * `:diff_delete_whitespace` - color of the whitespace deletions on diffs,
        defaults to `IO.ANSI.color_background(0, 2, 0)`;

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

    * `:failures_manifest_file` - specifies a path to the file used to store failures
      between runs;

    * `: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 tests to run in parallel. Only tests from
      different modules run in parallel. It defaults to `System.schedulers_online * 2`
      to optimize both CPU-bound and IO-bound tests;

    * `:max_failures` - the suite stops evaluating tests when this number of test failures
      is reached. All tests within a module that fail when using the `setup_all/1,2` callbacks
      are counted as failures. Defaults to `:infinity`;

    * `:only_test_ids` - a list of `{module_name, test_name}` tuples that limits
      what tests get run;

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

    * `:seed` - an integer seed value to randomize the test suite. This seed
      is also mixed with the test module and name to create a new unique seed
      on every test, which is automatically fed into the `:rand` module. This
      provides randomness between tests, but predictable and reproducible
      results. A `:seed` of `0` will disable randomization and the tests in each
      file will always run in the order that they were defined in;

    * `:slowest` - prints timing information for the N slowest tests. Running
      ExUnit with slow test reporting automatically runs in `trace` mode. It
      is disabled by default;

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

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

    * `: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 as timeout is set to `:infinity`.

    * `:test_location_relative_path` - the test location is the file:line information
      printed by tests as a shortcut to run a given test. When this value is set,
      the value is used as a prefix for the test itself. This is typically used by
      Mix to properly set-up umbrella projects

  Any arbitrary configuration can also be passed to `configure/1` or `start/1`,
  and these options can then be used in places such as custom formatters. These
  other options will be ignored by ExUnit itself.
  """
  @spec configure(Keyword.t()) :: :ok
  def configure(options) do
    Enum.each(options, fn {k, v} ->
      Application.put_env(:ex_unit, k, v)
    end)
  end

  @doc """
  Returns ExUnit configuration.
  """
  @spec configuration() :: Keyword.t()
  def configuration do
    Application.get_all_env(:ex_unit)
    |> put_seed()
    |> put_slowest()
    |> put_max_cases()
  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, the number of excluded tests and the number of skipped tests.
  """
  @spec run() :: suite_result()
  def run do
    _ = ExUnit.Server.modules_loaded()
    options = persist_defaults(configuration())
    ExUnit.Runner.run(options, nil)
  end

  @doc """
  Starts tests asynchronously while test cases are still loading.

  It returns a task that must be given to `await_run/0` when a result
  is desired.
  """
  @doc since: "1.12.0"
  @spec async_run() :: Task.t()
  def async_run() do
    Task.async(fn ->
      options = persist_defaults(configuration())
      ExUnit.Runner.run(options, nil)
    end)
  end

  @doc """
  Awaits for a test suite that has been started with `async_run/0`.
  """
  @doc since: "1.12.0"
  @spec await_run(Task.t()) :: suite_result()
  def await_run(task) do
    ExUnit.Server.modules_loaded()
    Task.await(task, :infinity)
  end

  @doc """
  Sets a callback to be executed after the completion of a test suite.

  Callbacks set with `after_suite/1` must accept a single argument, which is a
  map containing the results of the test suite's execution.

  If `after_suite/1` is called multiple times, the callbacks will be called in
  reverse order. In other words, the last callback set will be the first to be
  called.
  """
  @doc since: "1.8.0"
  @spec after_suite((suite_result() -> any)) :: :ok
  def after_suite(function) when is_function(function) do
    current_callbacks = Application.fetch_env!(:ex_unit, :after_suite)
    configure(after_suite: [function | current_callbacks])
  end

  @doc """
  Fetches the test supervisor for the current test.

  Returns `{:ok, supervisor_pid}` or `:error` if not called from the test process.

  This is the same supervisor as used by `ExUnit.Callbacks.start_supervised/2`
  and similar, see `ExUnit.Callbacks` module documentation for more information.
  """
  @doc since: "1.11.0"
  @spec fetch_test_supervisor() :: {:ok, pid()} | :error
  def fetch_test_supervisor() do
    case ExUnit.OnExitHandler.get_supervisor(self()) do
      {:ok, nil} ->
        opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
        {:ok, sup} = Supervisor.start_link([], opts)
        ExUnit.OnExitHandler.put_supervisor(self(), sup)
        {:ok, sup}

      {:ok, _} = ok ->
        ok

      :error ->
        :error
    end
  end

  # Persists default values in application
  # environment before the test suite starts.
  defp persist_defaults(config) do
    config |> Keyword.take([:max_cases, :seed, :trace]) |> configure()
    config
  end

  defp put_seed(opts) do
    Keyword.put_new_lazy(opts, :seed, fn ->
      # We're using `rem System.system_time()` here
      # instead of directly using :os.timestamp or using the
      # :microsecond argument because the VM on Windows has odd
      # precision. Calling with :microsecond will give us a multiple
      # of 1000. Calling without it gives actual microsecond precision.
      System.system_time()
      |> System.convert_time_unit(:native, :microsecond)
      |> rem(1_000_000)
    end)
  end

  defp put_max_cases(opts) do
    Keyword.put(opts, :max_cases, max_cases(opts))
  end

  defp put_slowest(opts) do
    if opts[:slowest] > 0 do
      Keyword.put(opts, :trace, true)
    else
      opts
    end
  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) Note that 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. Run `mix help test` for more information.
  """

  @typedoc """
  All tests start with a state of `nil`.

  A finished test can be in one of five states:

    1. Passed (also represented by `nil`)
    2. Failed
    3. Skipped (via @tag :skip)
    4. Excluded (via :exclude filters)
    5. Invalid (when setup_all fails)

  """
  @type state ::
          nil | {:excluded, binary} | {:failed, failed} | {:invalid, module} | {:skipped, binary}

  @typedoc "The error state returned by `ExUnit.Test` and `ExUnit.TestModule`"
  @type failed :: [{Exception.kind(), reason :: term, Exception.stacktrace()}]

  @typedoc "A map representing the results of running a test suite"
  @type suite_result :: %{
          excluded: non_neg_integer,
          failures: non_neg_integer,
          skipped: non_neg_integer,
          total: non_neg_integer
        }

  @type test_id :: {module, name :: atom}

  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
      * `:module` - the test module
      * `:state` - the finished test state (see `t:ExUnit.state/0`)
      * `:time` - the duration in microseconds of the test's runtime
      * `:tags` - the test tags
      * `:logs` - the captured logs

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

    # TODO: Remove the `:case` field on v2.0
    @type t :: %__MODULE__{
            name: atom,
            case: module,
            module: module,
            state: ExUnit.state(),
            time: non_neg_integer,
            tags: map,
            logs: String.t()
          }
  end

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

    It is received by formatters and contains the following fields:

      * `:file`  - (since v1.11.0) the file of the test module

      * `:name`  - the test module name

      * `:state` - the test error state (see `t:ExUnit.state/0`)

      * `:tests` - all tests in this module

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

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

  defmodule TestCase do
    # TODO: Remove this module on v2.0 (it has been replaced by TestModule)
    @moduledoc false
    defstruct [:name, :state, tests: []]

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

  defmodule TimeoutError do
    defexception [:timeout, :type]

    @impl true
    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" (accepts :infinity)
        2. per test module by setting "@moduletag timeout: x" (accepts :infinity)
        3. globally via "ExUnit.start(timeout: x)" configuration
        4. by running "mix test --timeout x" which sets timeout
        5. or by running "mix test --trace" which sets timeout to infinity
           (useful when using IEx.pry/0)

      where "x" is the timeout given as integer in milliseconds (defaults to 60_000).
      """
    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/0` to run tests.
  """
  @spec start(Keyword.t()) :: :ok
  def start(options \\ []) do
    {:ok, _} = Application.ensure_all_started(:ex_unit)

    configure(options)

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

      System.at_exit(fn
        0 ->
          time = ExUnit.Server.modules_loaded()
          options = persist_defaults(configuration())
          %{failures: failures} = ExUnit.Runner.run(options, time)

          System.at_exit(fn _ ->
            if failures > 0, do: exit({:shutdown, 1})
          end)

        _ ->
          :ok
      end)
    else
      :ok
    end
  end

  @doc """
  Configures ExUnit.

  ## Options

  ExUnit supports the following options:

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

    * `: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`;

    * `:colors` - a keyword list of color options to be used by some formatters:
      * `:enabled` - boolean option to enable colors, defaults to `IO.ANSI.enabled?/0`;
      * `:diff_insert` - color of the insertions on diffs, defaults to `:green`;
      * `:diff_insert_whitespace` - color of the whitespace insertions on diffs,
        defaults to `IO.ANSI.color_background(2, 0, 0)`;
      * `:diff_delete` - color of the deletions on diffs, defaults to `:red`;
      * `:diff_delete_whitespace` - color of the whitespace deletions on diffs,
        defaults to `IO.ANSI.color_background(0, 2, 0)`;

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

    * `:failures_manifest_file` - specifies a path to the file used to store failures
      between runs;

    * `: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 tests to run in parallel. Only tests from
      different modules run in parallel. It defaults to `System.schedulers_online * 2`
      to optimize both CPU-bound and IO-bound tests;

    * `:max_failures` - the suite stops evaluating tests when this number of test failures
      is reached. All tests within a module that fail when using the `setup_all/1,2` callbacks
      are counted as failures. Defaults to `:infinity`;

    * `:only_test_ids` - a list of `{module_name, test_name}` tuples that limits
      what tests get run;

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

    * `:seed` - an integer seed value to randomize the test suite. This seed
      is also mixed with the test module and name to create a new unique seed
      on every test, which is automatically fed into the `:rand` module. This
      provides randomness between tests, but predictable and reproducible
      results. A `:seed` of `0` will disable randomization and the tests in each
      file will always run in the order that they were defined in;

    * `:slowest` - prints timing information for the N slowest tests. Running
      ExUnit with slow test reporting automatically runs in `trace` mode. It
      is disabled by default;

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

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

    * `: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 as timeout is set to `:infinity`.

    * `:test_location_relative_path` - the test location is the file:line information
      printed by tests as a shortcut to run a given test. When this value is set,
      the value is used as a prefix for the test itself. This is typically used by
      Mix to properly set-up umbrella projects

  Any arbitrary configuration can also be passed to `configure/1` or `start/1`,
  and these options can then be used in places such as custom formatters. These
  other options will be ignored by ExUnit itself.
  """
  @spec configure(Keyword.t()) :: :ok
  def configure(options) do
    Enum.each(options, fn {k, v} ->
      Application.put_env(:ex_unit, k, v)
    end)
  end

  @doc """
  Returns ExUnit configuration.
  """
  @spec configuration() :: Keyword.t()
  def configuration do
    Application.get_all_env(:ex_unit)
    |> put_seed()
    |> put_slowest()
    |> put_max_cases()
  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, the number of excluded tests and the number of skipped tests.
  """
  @spec run() :: suite_result()
  def run do
    _ = ExUnit.Server.modules_loaded()
    options = persist_defaults(configuration())
    ExUnit.Runner.run(options, nil)
  end

  @doc """
  Starts tests asynchronously while test cases are still loading.

  It returns a task that must be given to `await_run/0` when a result
  is desired.
  """
  @doc since: "1.12.0"
  @spec async_run() :: Task.t()
  def async_run() do
    Task.async(fn ->
      options = persist_defaults(configuration())
      ExUnit.Runner.run(options, nil)
    end)
  end

  @doc """
  Awaits for a test suite that has been started with `async_run/0`.
  """
  @doc since: "1.12.0"
  @spec await_run(Task.t()) :: suite_result()
  def await_run(task) do
    ExUnit.Server.modules_loaded()
    Task.await(task, :infinity)
  end

  @doc """
  Sets a callback to be executed after the completion of a test suite.

  Callbacks set with `after_suite/1` must accept a single argument, which is a
  map containing the results of the test suite's execution.

  If `after_suite/1` is called multiple times, the callbacks will be called in
  reverse order. In other words, the last callback set will be the first to be
  called.
  """
  @doc since: "1.8.0"
  @spec after_suite((suite_result() -> any)) :: :ok
  def after_suite(function) when is_function(function) do
    current_callbacks = Application.fetch_env!(:ex_unit, :after_suite)
    configure(after_suite: [function | current_callbacks])
  end

  @doc """
  Fetches the test supervisor for the current test.

  Returns `{:ok, supervisor_pid}` or `:error` if not called from the test process.

  This is the same supervisor as used by `ExUnit.Callbacks.start_supervised/2`
  and similar, see `ExUnit.Callbacks` module documentation for more information.
  """
  @doc since: "1.11.0"
  @spec fetch_test_supervisor() :: {:ok, pid()} | :error
  def fetch_test_supervisor() do
    case ExUnit.OnExitHandler.get_supervisor(self()) do
      {:ok, nil} ->
        opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
        {:ok, sup} = Supervisor.start_link([], opts)
        ExUnit.OnExitHandler.put_supervisor(self(), sup)
        {:ok, sup}

      {:ok, _} = ok ->
        ok

      :error ->
        :error
    end
  end

  # Persists default values in application
  # environment before the test suite starts.
  defp persist_defaults(config) do
    config |> Keyword.take([:max_cases, :seed, :trace]) |> configure()
    config
  end

  defp put_seed(opts) do
    Keyword.put_new_lazy(opts, :seed, fn ->
      # We're using `rem System.system_time()` here
      # instead of directly using :os.timestamp or using the
      # :microsecond argument because the VM on Windows has odd
      # precision. Calling with :microsecond will give us a multiple
      # of 1000. Calling without it gives actual microsecond precision.
      System.system_time()
      |> System.convert_time_unit(:native, :microsecond)
      |> rem(1_000_000)
    end)
  end

  defp put_max_cases(opts) do
    Keyword.put(opts, :max_cases, max_cases(opts))
  end

  defp put_slowest(opts) do
    if opts[:slowest] > 0 do
      Keyword.put(opts, :trace, true)
    else
      opts
    end
  end

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


Increment version [ci skip]

Increment version [ci skip]


Bump version number

Bump version number


Update version

Update version


Web: Simplify controller with 404 errors where get_by fails. Fix #125

defmodule Krihelinator.PageController do
  use Krihelinator.Web, :controller

  def repositories(conn, _params) do
    repos = Repo.all from(r in GithubRepo,
                          order_by: [desc: r.krihelimeter],
                          limit: 50,
                          preload: :language)
    render conn, "repositories.html", repos: repos
  end

  def repository(conn, %{"repository" => repository_name}) do
    repository_name = Enum.join(repository_name, "/")
    case get_from_db_or_scrape(repository_name) do
      {:error, _whatever} ->
        conn
        |> put_flash(:error, "The repository \"#{repository_name}\" does not exist. Note that search is case sensitive.")
        |> redirect(to: page_path(conn, :repositories))
      {:ok, model} ->
        model = Repo.preload(model, :language)
        render(conn, "repository.html", repo: model)
    end
  end

  def language(conn, %{"language" => language_name}) do
    repos_query = from(r in GithubRepo,
                       order_by: [desc: r.krihelimeter],
                       limit: 50)

    language =
      Language
      |> Repo.get_by!(name: language_name)
      |> Repo.preload([repos: repos_query])

    render(conn, "language.html", language: language)
  end

  def languages(conn, _params) do
    query = from(l in Language,
                 order_by: [{:desc, :krihelimeter}],
                 where: l.krihelimeter > 0)
    languages = Repo.all(query)
    render(conn, "languages.html", languages: languages)
  end

  def languages_history(conn, params) do
    case Krihelinator.InputValidator.validate_history_query(params) do

      {:ok, language_names} ->

        query = from(h in Krihelinator.LanguageHistory,
                     order_by: :timestamp,
                     preload: :language)

        json =
          query
          |> Repo.all()
          |> Stream.filter(&(&1.language.name in language_names))
          |> Stream.map(&(
              %{name: &1.language.name,
                timestamp: &1.timestamp,
                value: &1.krihelimeter}
          ))
          |> Poison.encode!()
        render conn, "languages_history.html", json: json

      {:error, error} ->

        conn
        |> put_flash(:error, error)
        |> put_status(:bad_request)
        |> render(Krihelinator.ErrorView, "400.html")
    end

  end

  def showcases(conn, _params) do
    showcases = Repo.all(Showcase)
    render conn, "showcases.html", showcases: showcases
  end

  def showcase(conn, %{"showcase" => showcase_href}) do
    repos_query = from(r in GithubRepo,
                       order_by: [desc: r.krihelimeter],
                       preload: :language,
                       limit: 50)

    showcase =
      Showcase
      |> Repo.get_by!(href: showcase_href)
      |> Repo.preload([repos: repos_query])

    render(conn, "showcase.html", showcase: showcase)
  end

  def about(conn, _params) do
    render conn, "about.html"
  end
end

defmodule Krihelinator.PageController do
  use Krihelinator.Web, :controller

  def repositories(conn, _params) do
    repos = Repo.all from(r in GithubRepo,
                          order_by: [desc: r.krihelimeter],
                          limit: 50,
                          preload: :language)
    render conn, "repositories.html", repos: repos
  end

  def repository(conn, %{"repository" => repository_name}) do
    repository_name = Enum.join(repository_name, "/")
    case get_from_db_or_scrape(repository_name) do
      {:error, _whatever} ->
        conn
        |> put_flash(:error, "The repository \"#{repository_name}\" does not exist. Note that search is case sensitive.")
        |> redirect(to: page_path(conn, :repositories))
      {:ok, model} ->
        model = Repo.preload(model, :language)
        render(conn, "repository.html", repo: model)
    end
  end

  def language(conn, %{"language" => language_name}) do
    repos_query = from(r in GithubRepo,
                       order_by: [desc: r.krihelimeter],
                       limit: 50)
    language = Repo.one from(l in Language,
                             where: l.name == ^language_name,
                             preload: [repos: ^repos_query])

    case language do

      nil ->
        conn
        |> put_status(:not_found)
        |> put_layout(false)
        |> render(Krihelinator.ErrorView, "404.html", [])

      _otherwise ->
        render(conn, "language.html", language: language)

    end
  end

  def languages(conn, _params) do
    query = from(l in Language,
                 order_by: [{:desc, :krihelimeter}],
                 where: l.krihelimeter > 0)
    languages = Repo.all(query)
    render(conn, "languages.html", languages: languages)
  end

  def languages_history(conn, params) do
    case Krihelinator.InputValidator.validate_history_query(params) do

      {:ok, language_names} ->

        query = from(h in Krihelinator.LanguageHistory,
                     order_by: :timestamp,
                     preload: :language)

        json =
          query
          |> Repo.all()
          |> Stream.filter(&(&1.language.name in language_names))
          |> Stream.map(&(
              %{name: &1.language.name,
                timestamp: &1.timestamp,
                value: &1.krihelimeter}
          ))
          |> Poison.encode!()
        render conn, "languages_history.html", json: json

      {:error, error} ->

        conn
        |> put_flash(:error, error)
        |> put_status(:bad_request)
        |> render(Krihelinator.ErrorView, "400.html")
    end

  end

  def showcases(conn, _params) do
    showcases = Repo.all(Showcase)
    render conn, "showcases.html", showcases: showcases
  end

  def showcase(conn, %{"showcase" => showcase_href}) do
    repos_query = from(r in GithubRepo,
                       order_by: [desc: r.krihelimeter],
                       preload: :language,
                       limit: 50)
    showcase = Repo.one from(s in Showcase,
                             where: s.href == ^showcase_href,
                             preload: [repos: ^repos_query])

    case showcase do

      nil ->
        conn
        |> put_status(:not_found)
        |> put_layout(false)
        |> render(Krihelinator.ErrorView, "404.html", [])

      _otherwise ->
        render(conn, "showcase.html", showcase: showcase)

    end
  end

  def about(conn, _params) do
    render conn, "about.html"
  end
end


Fix some more warnings for unused variables

defmodule MarcoPolo.Protocol.RecordSerialization do
  @moduledoc false

  require Record

  Record.defrecordp :map_key, [:key, :data_type, :data_ptr]
  Record.defrecordp :typed_field, [:value, :type]

  defmodule Field do
    @type t :: %__MODULE__{
      name: binary,
      type: atom,
      pointer_to_data: non_neg_integer,
      value: term,
      encoded_value: iodata,
    }

    defstruct ~w(name type pointer_to_data value encoded_value)a
  end

  @doc """
  Parses a binary-serialized record.
  """
  @spec decode(binary) :: {non_neg_integer, String.t, %{}}
  def decode(data) do
    <<_version, rest :: binary>> = data
    rest |> decode_document |> elem(0)
  end

  @doc """
  Serializes a record using the binary serialization protocol.

  `class_name` is a string containing the class name of the record being
  encoded. `fields` is a list of `Field` structs.
  """
  def encode({class_name, fields}) do
    version_and_class = [0, encode_type(class_name || "", :string)]
    offset            = IO.iodata_length(version_and_class)
    encoded_fields    = encode_fields(fields, offset)

    [version_and_class, encoded_fields]
  end

  # Decodes a document (ODocument). This ODocument can be a "top-level" document
  # or an "embedded" type, since the leading serialization version byte is not
  # decoded here (but in `decode/1`).
  defp decode_document(data) do
    {class_name, rest}        = decode_type(data, :string)
    {field_definitions, rest} = decode_header(rest)
    {fields, rest}            = decode_fields(rest, field_definitions)

    if class_name == "" do
      class_name = nil
    end

    {{class_name, fields}, rest}
  end

  # Decodes an header returning a list of field definitions (which is a list of
  # `%Field{}` structs).
  defp decode_header(data, acc \\ []) do
    {i, rest} = :small_ints.decode_zigzag_varint(data)

    # If `i` is positive, that means the next field definition is a "named
    # field" and `i` is the length of the field's name. If it's negative, it
    # represents the property id of a property. If it's 0, it signals the end of
    # the header segment.
    cond do
      i == 0 ->
        # Remember to return `rest` and not `data` since `rest` doesn't contain
        # the 0 byte that signals the end of the header, while `data` does; we
        # want to ditch that byte.
        {Enum.reverse(acc), rest}
      i < 0 ->
         raise "properties aren't supported yet, only fields with a name and an explicit type"
      i > 0 ->
        {field, rest} = decode_field_definition(:named_field, data)
        decode_header(rest, [field|acc])
    end
  end

  # Decodes the definition of a named field in the header (`data`). Returns a
  # `%Field{}` struct with an empty value.
  defp decode_field_definition(:named_field, data) do
    {field_name, rest}            = decode_type(data, :string)
    {data_ptr, rest}              = decode_data_ptr(rest)
    <<data_type, rest :: binary>> = rest

    field = %Field{name: field_name, type: int_to_type(data_type), pointer_to_data: data_ptr}
    {field, rest}
  end

  # Decodes fields from the body of a serialized document (`data`) and a list of
  # `%Field{}` structs (with no `:value` field, they're definitions). Returns a
  # list of `%Field{}`s and the rest of the given data.
  defp decode_fields(data, field_definitions) do
    Enum.map_reduce field_definitions, data, fn(%Field{} = field, acc) ->
      if field.pointer_to_data == 0 do
        {%{field | value: nil}, acc}
      else
        {value, rest} = decode_type(acc, field.type)
        {%{field | value: value}, rest}
      end
    end
  end

  # The pointer to the data is just a signed int32.
  defp decode_data_ptr(data) do
    <<data_ptr :: 32-signed, rest :: binary>> = data
    {data_ptr, rest}
  end

  # Decodes an instance of `type` from `data`.
  defp decode_type(data, type)

  defp decode_type(<<0>> <> rest, :boolean), do: {false, rest}
  defp decode_type(<<1>> <> rest, :boolean), do: {true, rest}

  defp decode_type(data, type) when type in [:sint16, :sint32, :sint64] do
    :small_ints.decode_zigzag_varint(data)
  end

  defp decode_type(data, :float) do
    <<float :: 32-float, rest :: binary>> = data
    {float, rest}
  end

  defp decode_type(data, :double) do
    <<double :: 64-float, rest :: binary>> = data
    {double, rest}
  end

  defp decode_type(data, type) when type in [:string, :bytes] do
    {len, rest} = :small_ints.decode_zigzag_varint(data)
    len = len * 8
    <<string :: bits-size(len), rest :: binary>> = rest
    {string, rest}
  end

  defp decode_type(data, :embedded) do
    decode_document(data)
  end

  defp decode_type(data, type) when type in [:embedded_list, :embedded_set] do
    {nitems, rest} = :small_ints.decode_zigzag_varint(data)
    <<type, rest :: binary>> = rest

    # Only ANY is supported by OrientDB at the moment.
    :any = int_to_type(type)

    # OPTIMIZE: I have to find a better (clean) way to `map_reduce` n times
    # instead of mapreducing over a list of n times the number 0, which is
    # uselessly expensive to build and plain useless. A range doesn't work
    # because `Enum.to_list(1..0)` is `[1, 0]` which makes sense, but my
    # 1..nitems has to translate to `[]` so that the mapreducing doesn't
    # actually happen.
    Enum.map_reduce List.duplicate(0, nitems), rest, fn(_, acc) ->
      <<type, acc :: binary>> = acc
      decode_type(acc, int_to_type(type))
    end
  end

  defp decode_type(data, :embedded_map) do
    {keys, rest}   = decode_map_header(data)
    {keys_and_values, rest} = decode_map_values(rest, keys)

    {Enum.into(keys_and_values, %{}), rest}
  end

  defp decode_map_header(data) do
    {nkeys, rest} = :small_ints.decode_zigzag_varint(data)

    Enum.map_reduce List.duplicate(0, nkeys), rest, fn(_, <<string_type, acc :: binary>>) ->
      # For now, OrientDB only supports STRING keys.
      :string = int_to_type(string_type)

      {key, acc} = decode_type(acc, :string)
      {ptr, acc} = decode_data_ptr(acc)
      IO.inspect ptr
      <<type, acc :: binary>> = acc

      {map_key(key: key, data_type: int_to_type(type), data_ptr: ptr), acc}
    end
  end

  defp decode_map_values(data, keys) do
    Enum.map_reduce keys, data, fn(map_key(key: key_name, data_type: type, data_ptr: ptr), acc) ->
      if ptr == 0 do
        {{key_name, nil}, acc}
      else
        {value, acc} = decode_type(acc, type)
        {{key_name, value}, acc}
      end
    end
  end

  defp encode_fields(fields, offset) do
    offset = offset + header_offset(fields)

    {fields, values, _} = Enum.reduce fields, {[], [], offset}, fn(%Field{} = field, {fs, vs, index}) ->
      encoded_value = encode_field_value(field, index).encoded_value
      encoded_field = %{field | pointer_to_data: index} |> encode_field_for_header
      index         = index + IO.iodata_length(encoded_value)

      {[encoded_field|fs], [encoded_value|vs], index}
    end

    [Enum.reverse(fields), 0, Enum.reverse(values)]
  end

  # Returns the length of the header based on the list of fields.
  defp header_offset(fields) do
    # The last +1 is for the `0` that signals the end of the header.
    fields
    |> Enum.map(&encode_field_value(&1, 0))
    |> Enum.map(&(&1 |> encode_field_for_header |> IO.iodata_length))
    |> Enum.sum
    |> +(1)
  end

  # Returns the given `%Field{}` with the `:encoded_value` field set to the
  # result of encoding the `:value` field.
  defp encode_field_value(%Field{type: type, value: value} = field, offset) do
    %{field | encoded_value: encode_type(value, type, offset)}
  end

  # Encodes the given `%Field{}` for the header, i.e., just the field
  # representation and not the value (name, pointer to data, type). Returns
  # iodata.
  defp encode_field_for_header(%Field{pointer_to_data: ptr, type: type, name: name}) do
    if is_nil(ptr) do
      ptr = 0
    end

    [encode_type(name, :string), <<ptr :: 32-signed>>, type_to_int(type)]
  end

  # Encodes an instance of `type`. Returns an iodata instead of a binary.
  # Made public for testing.
  @doc false
  def encode_type(data, type, offset \\ 0)

  def encode_type(str, :string, _) when is_binary(str) do
    [:small_ints.encode_zigzag_varint(byte_size(str)), str]
  end

  def encode_type(i, type, _) when type in [:sint16, :sint32, :sint64] and is_integer(i) do
    :small_ints.encode_zigzag_varint(i)
  end

  def encode_type(map, :embedded_map, offset) when is_map(map) do
    offset = offset + map_header_offset(map)

    {keys, values, _} = Enum.reduce map, {[], [], offset}, fn({key, value}, {ks, vs, index}) ->
      typed_field(value: value, type: type) = value
      encoded_value = encode_type(value, type)
      key = [type_to_int(:string), encode_type(key, :string), <<index :: 32-signed>>, type_to_int(type)]
      index = index + IO.iodata_length(encoded_value)
      {[key|ks], [encoded_value|vs], index}
    end

    nkeys = map |> Map.keys |> Enum.count |> :small_ints.encode_zigzag_varint

    [nkeys, keys, values]
  end

  defp map_header_offset(map) do
    keys = Map.keys(map)

    # `6` means 4 bytes for the pointer to the data, 1 byte for the data type,
    # and 1 byte for the key type.
    nkeys       = :small_ints.encode_zigzag_varint(Enum.count(keys))
    key_lengths = Enum.map(keys, &(IO.iodata_length(encode_type(&1, :string)) + 6))

    byte_size(nkeys) + Enum.sum(key_lengths)
  end

  # http://orientdb.com/docs/last/Types.html
  @types [
    boolean: 0,
    sint32: 1,
    sint16: 2,
    sint64: 3,
    float: 4,
    double: 5,
    datetime: 6,
    string: 7,
    binary: 8,
    embedded: 9,
    embedded_list: 10,
    embedded_set: 11,
    embedded_map: 12,
    link: 13,
    link_list: 14,
    link_set: 15,
    link_map: 16,
    byte: 17,
    transient: 18,
    date: 19,
    custom: 20,
    decimal: 21,
    link_bag: 22,
    any: 23,
  ]

  for {type_name, type_id} <- @types do
    defp int_to_type(unquote(type_id)), do: unquote(type_name)
    defp type_to_int(unquote(type_name)), do: unquote(type_id)
  end
end

defmodule MarcoPolo.Protocol.RecordSerialization do
  @moduledoc false

  require Record

  Record.defrecordp :map_key, [:key, :data_type, :data_ptr]
  Record.defrecordp :typed_field, [:value, :type]

  defmodule Field do
    @type t :: %__MODULE__{
      name: binary,
      type: atom,
      pointer_to_data: non_neg_integer,
      value: term,
      encoded_value: iodata,
    }

    defstruct ~w(name type pointer_to_data value encoded_value)a
  end

  @doc """
  Parses a binary-serialized record.
  """
  @spec decode(binary) :: {non_neg_integer, String.t, %{}}
  def decode(data) do
    <<_version, rest :: binary>> = data
    rest |> decode_document |> elem(0)
  end

  @doc """
  Serializes a record using the binary serialization protocol.

  `class_name` is a string containing the class name of the record being
  encoded. `fields` is a list of `Field` structs.
  """
  def encode({class_name, fields}) do
    version_and_class = [0, encode_type(class_name || "", :string)]
    offset            = IO.iodata_length(version_and_class)
    encoded_fields    = encode_fields(fields, offset)

    [version_and_class, encoded_fields]
  end

  # Decodes a document (ODocument). This ODocument can be a "top-level" document
  # or an "embedded" type, since the leading serialization version byte is not
  # decoded here (but in `decode/1`).
  defp decode_document(data) do
    {class_name, rest}        = decode_type(data, :string)
    {field_definitions, rest} = decode_header(rest)
    {fields, rest}            = decode_fields(rest, field_definitions)

    if class_name == "" do
      class_name = nil
    end

    {{class_name, fields}, rest}
  end

  # Decodes an header returning a list of field definitions (which is a list of
  # `%Field{}` structs).
  defp decode_header(data, acc \\ []) do
    {i, rest} = :small_ints.decode_zigzag_varint(data)

    # If `i` is positive, that means the next field definition is a "named
    # field" and `i` is the length of the field's name. If it's negative, it
    # represents the property id of a property. If it's 0, it signals the end of
    # the header segment.
    cond do
      i == 0 ->
        # Remember to return `rest` and not `data` since `rest` doesn't contain
        # the 0 byte that signals the end of the header, while `data` does; we
        # want to ditch that byte.
        {Enum.reverse(acc), rest}
      i < 0 ->
         raise "properties aren't supported yet, only fields with a name and an explicit type"
      i > 0 ->
        {field, rest} = decode_field_definition(:named_field, data)
        decode_header(rest, [field|acc])
    end
  end

  # Decodes the definition of a named field in the header (`data`). Returns a
  # `%Field{}` struct with an empty value.
  defp decode_field_definition(:named_field, data) do
    {field_name, rest}            = decode_type(data, :string)
    {data_ptr, rest}              = decode_data_ptr(rest)
    <<data_type, rest :: binary>> = rest

    field = %Field{name: field_name, type: int_to_type(data_type), pointer_to_data: data_ptr}
    {field, rest}
  end

  # Decodes fields from the body of a serialized document (`data`) and a list of
  # `%Field{}` structs (with no `:value` field, they're definitions). Returns a
  # list of `%Field{}`s and the rest of the given data.
  defp decode_fields(data, field_definitions) do
    {fields, rest} = Enum.map_reduce field_definitions, data, fn(%Field{} = field, acc) ->
      if field.pointer_to_data == 0 do
        {%{field | value: nil}, acc}
      else
        {value, rest} = decode_type(acc, field.type)
        {%{field | value: value}, rest}
      end
    end
  end

  # The pointer to the data is just a signed int32.
  defp decode_data_ptr(data) do
    <<data_ptr :: 32-signed, rest :: binary>> = data
    {data_ptr, rest}
  end

  # Decodes an instance of `type` from `data`.
  defp decode_type(data, type)

  defp decode_type(<<0>> <> rest, :boolean), do: {false, rest}
  defp decode_type(<<1>> <> rest, :boolean), do: {true, rest}

  defp decode_type(data, type) when type in [:sint16, :sint32, :sint64] do
    :small_ints.decode_zigzag_varint(data)
  end

  defp decode_type(data, :float) do
    <<float :: 32-float, rest :: binary>> = data
    {float, rest}
  end

  defp decode_type(data, :double) do
    <<double :: 64-float, rest :: binary>> = data
    {double, rest}
  end

  defp decode_type(data, type) when type in [:string, :bytes] do
    {len, rest} = :small_ints.decode_zigzag_varint(data)
    len = len * 8
    <<string :: bits-size(len), rest :: binary>> = rest
    {string, rest}
  end

  defp decode_type(data, :embedded) do
    decode_document(data)
  end

  defp decode_type(data, type) when type in [:embedded_list, :embedded_set] do
    {nitems, rest} = :small_ints.decode_zigzag_varint(data)
    <<type, rest :: binary>> = rest

    # Only ANY is supported by OrientDB at the moment.
    :any = int_to_type(type)

    # OPTIMIZE: I have to find a better (clean) way to `map_reduce` n times
    # instead of mapreducing over a list of n times the number 0, which is
    # uselessly expensive to build and plain useless. A range doesn't work
    # because `Enum.to_list(1..0)` is `[1, 0]` which makes sense, but my
    # 1..nitems has to translate to `[]` so that the mapreducing doesn't
    # actually happen.
    Enum.map_reduce List.duplicate(0, nitems), rest, fn(_, acc) ->
      <<type, acc :: binary>> = acc
      decode_type(acc, int_to_type(type))
    end
  end

  defp decode_type(data, :embedded_map) do
    {keys, rest}   = decode_map_header(data)
    {keys_and_values, rest} = decode_map_values(rest, keys)

    {Enum.into(keys_and_values, %{}), rest}
  end

  defp decode_map_header(data) do
    {nkeys, rest} = :small_ints.decode_zigzag_varint(data)

    {keys, rest} = Enum.map_reduce List.duplicate(0, nkeys), rest, fn(_, <<string_type, acc :: binary>>) ->
      # For now, OrientDB only supports STRING keys.
      :string = int_to_type(string_type)

      {key, acc} = decode_type(acc, :string)
      {ptr, acc} = decode_data_ptr(acc)
      IO.inspect ptr
      <<type, acc :: binary>> = acc

      {map_key(key: key, data_type: int_to_type(type), data_ptr: ptr), acc}
    end
  end

  defp decode_map_values(data, keys) do
    Enum.map_reduce keys, data, fn(map_key(key: key_name, data_type: type, data_ptr: ptr), acc) ->
      if ptr == 0 do
        {{key_name, nil}, acc}
      else
        {value, acc} = decode_type(acc, type)
        {{key_name, value}, acc}
      end
    end
  end

  defp encode_fields(fields, offset) do
    offset = offset + header_offset(fields)

    {fields, values, _} = Enum.reduce fields, {[], [], offset}, fn(%Field{} = field, {fs, vs, index}) ->
      encoded_value = encode_field_value(field, index).encoded_value
      encoded_field = %{field | pointer_to_data: index} |> encode_field_for_header
      index         = index + IO.iodata_length(encoded_value)

      {[encoded_field|fs], [encoded_value|vs], index}
    end

    [Enum.reverse(fields), 0, Enum.reverse(values)]
  end

  # Returns the length of the header based on the list of fields.
  defp header_offset(fields) do
    # The last +1 is for the `0` that signals the end of the header.
    fields
    |> Enum.map(&encode_field_value(&1, 0))
    |> Enum.map(&(&1 |> encode_field_for_header |> IO.iodata_length))
    |> Enum.sum
    |> +(1)
  end

  # Returns the given `%Field{}` with the `:encoded_value` field set to the
  # result of encoding the `:value` field.
  defp encode_field_value(%Field{type: type, value: value} = field, offset) do
    %{field | encoded_value: encode_type(value, type, offset)}
  end

  # Encodes the given `%Field{}` for the header, i.e., just the field
  # representation and not the value (name, pointer to data, type). Returns
  # iodata.
  defp encode_field_for_header(%Field{pointer_to_data: ptr, type: type, name: name} = field) do
    if is_nil(ptr) do
      ptr = 0
    end

    [encode_type(name, :string), <<ptr :: 32-signed>>, type_to_int(type)]
  end

  # Encodes an instance of `type`. Returns an iodata instead of a binary.
  # Made public for testing.
  @doc false
  def encode_type(data, type, offset \\ 0)

  def encode_type(str, :string, _) when is_binary(str) do
    [:small_ints.encode_zigzag_varint(byte_size(str)), str]
  end

  def encode_type(i, type, _) when type in [:sint16, :sint32, :sint64] and is_integer(i) do
    :small_ints.encode_zigzag_varint(i)
  end

  def encode_type(map, :embedded_map, offset) when is_map(map) do
    offset = offset + map_header_offset(map)

    {keys, values, _} = Enum.reduce map, {[], [], offset}, fn({key, value}, {ks, vs, index}) ->
      typed_field(value: value, type: type) = value
      encoded_value = encode_type(value, type)
      key = [type_to_int(:string), encode_type(key, :string), <<index :: 32-signed>>, type_to_int(type)]
      index = index + IO.iodata_length(encoded_value)
      {[key|ks], [encoded_value|vs], index}
    end

    nkeys = map |> Map.keys |> Enum.count |> :small_ints.encode_zigzag_varint

    [nkeys, keys, values]
  end

  defp map_header_offset(map) do
    keys = Map.keys(map)

    # `6` means 4 bytes for the pointer to the data, 1 byte for the data type,
    # and 1 byte for the key type.
    nkeys       = :small_ints.encode_zigzag_varint(Enum.count(keys))
    key_lengths = Enum.map(keys, &(IO.iodata_length(encode_type(&1, :string)) + 6))

    byte_size(nkeys) + Enum.sum(key_lengths)
  end

  # http://orientdb.com/docs/last/Types.html
  @types [
    boolean: 0,
    sint32: 1,
    sint16: 2,
    sint64: 3,
    float: 4,
    double: 5,
    datetime: 6,
    string: 7,
    binary: 8,
    embedded: 9,
    embedded_list: 10,
    embedded_set: 11,
    embedded_map: 12,
    link: 13,
    link_list: 14,
    link_set: 15,
    link_map: 16,
    byte: 17,
    transient: 18,
    date: 19,
    custom: 20,
    decimal: 21,
    link_bag: 22,
    any: 23,
  ]

  for {type_name, type_id} <- @types do
    defp int_to_type(unquote(type_id)), do: unquote(type_name)
    defp type_to_int(unquote(type_name)), do: unquote(type_id)
  end
end