Handle new significant key in sasl reports

defmodule Logger.Translator do
  @moduledoc """
  Default translation for Erlang log messages.

  Logger allows developers to rewrite log messages provided by
  OTP applications into a format more compatible with Elixir
  log messages by providing a translator.

  A translator is simply a tuple containing a module and a function
  that can be added and removed via the `Logger.add_translator/1` and
  `Logger.remove_translator/1` functions and is invoked for every Erlang
  message above the minimum log level with four arguments:

    * `min_level` - the current Logger level
    * `level` - the level of the message being translated
    * `kind` - if the message is a `:report` or `:format`
    * `message` - the message to format. If it is `:report`, it is a tuple
      with `{report_type, report_data}`, if it is `:format`, it is a
      tuple with `{format_message, format_args}`.

  The function must return:

    * `{:ok, chardata, metadata}` - if the message translation with its metadata
    * `{:ok, chardata}` - the translated message
    * `:skip` - if the message is not meant to be translated nor logged
    * `:none` - if there is no translation, which triggers the next translator

  See the function `translate/4` in this module for an example implementation
  and the default messages translated by Logger.
  """

  @doc """
  Built-in translation function.
  """
  def translate(min_level, level, kind, message)

  def translate(min_level, _level, :report, {:logger, %{label: label} = report}) do
    case label do
      {:gen_server, :terminate} ->
        report_gen_server_terminate(min_level, report)

      {:gen_event, :terminate} ->
        report_gen_event_terminate(min_level, report)

      _ ->
        :skip
    end
  end

  def translate(min_level, _level, :report, {{:proc_lib, :crash}, data}) do
    report_crash(min_level, data)
  end

  def translate(min_level, _level, :report, {{:supervisor, :progress}, data}) do
    report_supervisor_progress(min_level, data)
  end

  def translate(min_level, _level, :report, {{:supervisor, _}, data}) do
    report_supervisor(min_level, data)
  end

  def translate(
        _min_level,
        _level,
        :report,
        {{:application_controller, :progress}, [application: app, started_at: node]}
      ) do
    {:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
  end

  def translate(
        _min_level,
        _level,
        :report,
        {{:application_controller, :exit}, [application: app, exited: reason, type: _type]}
      ) do
    {:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
  end

  def translate(
        _min_level,
        :error,
        :report,
        {{Task.Supervisor, :terminating},
         %{
           name: name,
           starter: starter,
           function: function,
           args: args,
           reason: reason
         }}
      ) do
    opts = Application.get_env(:logger, :translator_inspect_opts)

    {formatted, reason} = format_reason(reason)
    metadata = [crash_reason: reason] ++ registered_name(name)

    msg =
      ["Task #{inspect(name)} started from #{inspect(starter)} terminating"] ++
        [formatted, "\nFunction: #{inspect(function, opts)}"] ++
        ["\n    Args: #{inspect(args, opts)}"]

    {:ok, msg, metadata}
  end

  def translate(min_level, :error, :format, message) do
    case message do
      # This is no longer emitted by Erlang/OTP but it may be
      # manually emitted by libraries like connection.
      # TODO: Remove this translation on Elixir v1.14
      {'** Generic server ' ++ _, [name, last, state, reason | client]} ->
        opts = Application.get_env(:logger, :translator_inspect_opts)
        {formatted, reason} = format_reason(reason)
        metadata = [crash_reason: reason] ++ registered_name(name)

        msg =
          ["GenServer #{inspect(name)} terminating", formatted] ++
            ["\nLast message#{format_from(client)}: #{inspect(last, opts)}"]

        if min_level == :debug do
          msg = [msg, "\nState: #{inspect(state, opts)}" | format_client(client)]
          {:ok, msg, metadata}
        else
          {:ok, msg, metadata}
        end

      {'Error in process ' ++ _, [pid, node, {reason, stack}]} ->
        reason = Exception.normalize(:error, reason, stack)

        msg = [
          "Process ",
          inspect(pid),
          " on node ",
          inspect(node),
          " raised an exception" | format(:error, reason, stack)
        ]

        {:ok, msg, [crash_reason: exit_reason(:error, reason, stack)]}

      {'Error in process ' ++ _, [pid, {reason, stack}]} ->
        reason = Exception.normalize(:error, reason, stack)
        msg = ["Process ", inspect(pid), " raised an exception" | format(:error, reason, stack)]
        {:ok, msg, [crash_reason: exit_reason(:error, reason, stack)]}

      _ ->
        :none
    end
  end

  def translate(_min_level, :info, :report, {
        :std_info,
        [application: app, exited: reason, type: _type]
      }) do
    {:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
  end

  def translate(min_level, :error, :report, {{:error_logger, :error_report}, data}) do
    report_supervisor(min_level, data)
  end

  def translate(min_level, :error, :report, {:supervisor_report, data}) do
    report_supervisor(min_level, data)
  end

  def translate(min_level, :error, :report, {:crash_report, data}) do
    report_crash(min_level, data)
  end

  def translate(min_level, :info, :report, {:progress, [{:supervisor, _} | _] = data}) do
    report_supervisor_progress(min_level, data)
  end

  def translate(_min_level, :info, :report, {:progress, [application: app, started_at: node]}) do
    {:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
  end

  ## Helpers

  def translate(_min_level, _level, _kind, _message) do
    :none
  end

  defp report_gen_server_terminate(min_level, report) do
    inspect_opts = Application.get_env(:logger, :translator_inspect_opts)

    %{
      client_info: client,
      last_message: last,
      name: name,
      reason: reason,
      state: state
    } = report

    {formatted, reason} = format_reason(reason)
    metadata = [crash_reason: reason] ++ registered_name(name)

    msg =
      ["GenServer ", inspect(name), " terminating", formatted] ++
        ["\nLast message", format_last_message_from(client), ": ", inspect(last, inspect_opts)]

    if min_level == :debug do
      msg = [msg, "\nState: ", inspect(state, inspect_opts) | format_client_info(client)]
      {:ok, msg, metadata}
    else
      {:ok, msg, metadata}
    end
  end

  defp report_gen_event_terminate(min_level, report) do
    inspect_opts = Application.get_env(:logger, :translator_inspect_opts)

    %{
      handler: handler,
      last_message: last,
      name: name,
      reason: reason,
      state: state
    } = report

    reason =
      case reason do
        {:EXIT, why} -> why
        _ -> reason
      end

    {formatted, reason} = format_reason(reason)
    metadata = [crash_reason: reason] ++ registered_name(name)

    msg =
      [":gen_event handler ", inspect(handler), " installed in ", inspect(name), " terminating"] ++
        [formatted, "\nLast message: ", inspect(last, inspect_opts)]

    if min_level == :debug do
      {:ok, [msg, "\nState: ", inspect(state, inspect_opts)], metadata}
    else
      {:ok, msg, metadata}
    end
  end

  defp report_supervisor_progress(
         min_level,
         supervisor: sup,
         started: [{:pid, pid}, {:id, id} | started]
       ) do
    msg =
      ["Child ", inspect(id), " of Supervisor ", sup_name(sup), " started"] ++
        ["\nPid: ", inspect(pid)] ++ child_info(min_level, started)

    {:ok, msg}
  end

  defp report_supervisor_progress(
         min_level,
         supervisor: sup,
         started: [{:pid, pid} | started]
       ) do
    msg =
      ["Child of Supervisor ", sup_name(sup), " started", "\nPid: ", inspect(pid)] ++
        child_info(min_level, started)

    {:ok, msg}
  end

  defp report_supervisor_progress(_min_level, _other), do: :none

  defp report_supervisor(
         min_level,
         supervisor: sup,
         errorContext: context,
         reason: reason,
         offender: [{:pid, pid}, {:id, id} | offender]
       ) do
    pid_info =
      if is_pid(pid) and context != :shutdown do
        ["\nPid: ", inspect(pid)]
      else
        []
      end

    msg =
      ["Child ", inspect(id), " of Supervisor ", sup_name(sup)] ++
        [?\s, sup_context(context), "\n** (exit) ", offender_reason(reason, context)] ++
        pid_info ++ child_info(min_level, offender)

    {:ok, msg}
  end

  defp report_supervisor(
         min_level,
         supervisor: sup,
         errorContext: context,
         reason: reason,
         offender: [{:nb_children, n}, {:id, id} | offender]
       ) do
    msg =
      ["Children ", inspect(id), " of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
        ["\n** (exit) ", offender_reason(reason, context), "\nNumber: ", Integer.to_string(n)] ++
        child_info(min_level, offender)

    {:ok, msg}
  end

  defp report_supervisor(
         min_level,
         supervisor: sup,
         errorContext: context,
         reason: reason,
         offender: [{:pid, pid} | offender]
       ) do
    msg =
      ["Child of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
        ["\n** (exit) ", offender_reason(reason, context), "\nPid: ", inspect(pid)] ++
        child_info(min_level, offender)

    {:ok, msg}
  end

  defp report_supervisor(_min_level, _other), do: :none

  # If start call raises reason will be of form {:EXIT, reason}
  defp offender_reason({:EXIT, reason}, :start_error) do
    Exception.format_exit(reason)
  end

  defp offender_reason(reason, _context) do
    Exception.format_exit(reason)
  end

  defp sup_name({:local, name}), do: inspect(name)
  defp sup_name({:global, name}), do: inspect(name)
  defp sup_name({:via, _mod, name}), do: inspect(name)
  defp sup_name({pid, mod}), do: [inspect(pid), " (", inspect(mod), ?)]
  defp sup_name(unknown_name), do: inspect(unknown_name)

  defp sup_context(:start_error), do: "failed to start"
  defp sup_context(:child_terminated), do: "terminated"
  defp sup_context(:shutdown), do: "caused shutdown"
  defp sup_context(:shutdown_error), do: "shut down abnormally"

  defp child_info(min_level, [{:mfargs, {mod, fun, args}} | debug]) do
    ["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
  end

  # Comes from bridge with MFA
  defp child_info(min_level, [{:mfa, {mod, fun, args}} | debug]) do
    ["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
  end

  # Comes from bridge with Mod
  defp child_info(min_level, [{:mod, mod} | debug]) do
    ["\nStart Module: ", inspect(mod) | child_debug(min_level, debug)]
  end

  defp child_info(_min_level, _child) do
    []
  end

  defp child_debug(:debug, opts) do
    for {key, value} <- opts do
      child_debug_key(key, value)
    end
  end

  defp child_debug(_min_level, _child) do
    []
  end

  defp child_debug_key(:restart_type, value), do: ["\nRestart: " | inspect(value)]
  defp child_debug_key(:shutdown, value), do: ["\nShutdown: " | inspect(value)]
  defp child_debug_key(:child_type, value), do: ["\nType: " | inspect(value)]
  defp child_debug_key(:significant, value), do: if(value, do: "\nSignificant: true", else: [])
  defp child_debug_key(_, _), do: []

  defp report_crash(min_level, [[{:initial_call, initial_call} | crashed], linked]) do
    mfa = initial_call_to_mfa(initial_call)
    report_crash(min_level, crashed, [{:initial_call, mfa}], linked)
  end

  defp report_crash(min_level, [crashed, linked]) do
    report_crash(min_level, crashed, [], linked)
  end

  defp report_crash(min_level, crashed, extra, linked) do
    [
      {:pid, pid},
      {:registered_name, name},
      {:error_info, {kind, reason, stack}} | crashed
    ] = crashed

    dictionary = crashed[:dictionary]
    reason = Exception.normalize(kind, reason, stack)

    case Keyword.get(dictionary, :logger_enabled, true) do
      false ->
        :skip

      true ->
        user_metadata = Keyword.get(dictionary, :"$logger_metadata$", %{}) |> Map.to_list()

        msg =
          ["Process ", crash_name(pid, name), " terminating", format(kind, reason, stack)] ++
            [crash_info(min_level, extra ++ crashed, [?\n]), crash_linked(min_level, linked)]

        extra =
          if ancestors = crashed[:ancestors], do: [{:ancestors, ancestors} | extra], else: extra

        extra =
          if callers = dictionary[:"$callers"], do: [{:callers, callers} | extra], else: extra

        extra = [{:crash_reason, exit_reason(kind, reason, stack)} | extra]
        {:ok, msg, registered_name(name) ++ extra ++ user_metadata}
    end
  end

  defp initial_call_to_mfa({:supervisor, module, _}), do: {module, :init, 1}
  defp initial_call_to_mfa({:supervisor_bridge, module, _}), do: {module, :init, 1}
  defp initial_call_to_mfa({mod, fun, args}) when is_list(args), do: {mod, fun, length(args)}
  defp initial_call_to_mfa(mfa), do: mfa

  defp crash_name(pid, []), do: inspect(pid)
  defp crash_name(pid, name), do: [inspect(name), " (", inspect(pid), ?)]

  defp crash_info(min_level, [{:initial_call, {mod, fun, args}} | info], prefix) do
    [prefix, "Initial Call: ", crash_call(mod, fun, args) | crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level, [{:current_function, {mod, fun, args}} | info], prefix) do
    [prefix, "Current Call: ", crash_call(mod, fun, args) | crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level, [{:current_function, []} | info], prefix) do
    crash_info(min_level, info, prefix)
  end

  defp crash_info(min_level, [{:ancestors, ancestors} | debug], prefix) do
    [prefix, "Ancestors: ", inspect(ancestors) | crash_info(min_level, debug, prefix)]
  end

  defp crash_info(:debug, debug, prefix) do
    for {key, value} <- debug do
      crash_debug(key, value, prefix)
    end
  end

  defp crash_info(_, _, _) do
    []
  end

  defp crash_call(mod, fun, arity) when is_integer(arity) do
    format_mfa(mod, fun, arity)
  end

  defp crash_call(mod, fun, args) do
    format_mfa(mod, fun, length(args))
  end

  defp crash_debug(:current_stacktrace, stack, prefix) do
    stack_prefix = [prefix | "    "]
    stacktrace = Enum.map(stack, &[stack_prefix | Exception.format_stacktrace_entry(&1)])
    [prefix, "Current Stacktrace:" | stacktrace]
  end

  defp crash_debug(key, value, prefix) do
    [prefix, crash_debug_key(key), ?:, ?\s, inspect(value)]
  end

  defp crash_debug_key(key) do
    case key do
      :message_queue_len -> "Message Queue Length"
      :messages -> "Messages"
      :links -> "Links"
      :dictionary -> "Dictionary"
      :trap_exit -> "Trapping Exits"
      :status -> "Status"
      :heap_size -> "Heap Size"
      :stack_size -> "Stack Size"
      :reductions -> "Reductions"
    end
  end

  defp crash_linked(_min_level, []), do: []

  defp crash_linked(min_level, neighbours) do
    Enum.reduce(neighbours, "\nNeighbours:", fn {:neighbour, info}, acc ->
      [acc | crash_neighbour(min_level, info)]
    end)
  end

  @indent "    "

  defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, []} | info]) do
    [?\n, @indent, inspect(pid) | crash_info(min_level, info, [?\n, @indent | @indent])]
  end

  defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, name} | info]) do
    [?\n, @indent, inspect(name), " (", inspect(pid), ")"] ++
      crash_info(min_level, info, [?\n, @indent | @indent])
  end

  defp format_last_message_from({_, {name, _}}), do: [" (from ", inspect(name), ")"]
  defp format_last_message_from({from, _}), do: [" (from ", inspect(from), ")"]
  defp format_last_message_from(_), do: []

  defp format_client_info({from, :dead}),
    do: ["\nClient ", inspect(from), " is dead"]

  defp format_client_info({from, :remote}),
    do: ["\nClient ", inspect(from), " is remote on node ", inspect(node(from))]

  defp format_client_info({_, {name, stacktrace}}),
    do: ["\nClient ", inspect(name), " is alive\n" | format_stacktrace(stacktrace)]

  defp format_client_info(_),
    do: []

  defp format_reason({maybe_exception, [_ | _] = maybe_stacktrace} = reason) do
    try do
      format_stacktrace(maybe_stacktrace)
    catch
      :error, _ ->
        {format_stop(reason), {reason, []}}
    else
      formatted_stacktrace ->
        {formatted, reason} = maybe_normalize(maybe_exception, maybe_stacktrace)
        {[formatted | formatted_stacktrace], {reason, maybe_stacktrace}}
    end
  end

  defp format_reason(reason) do
    {format_stop(reason), {reason, []}}
  end

  defp format_stop(reason) do
    ["\n** (stop) " | Exception.format_exit(reason)]
  end

  # Erlang processes rewrite the :undef error to these reasons when logging
  @gen_undef [:"module could not be loaded", :"function not exported"]

  defp maybe_normalize(undef, [{mod, fun, args, _info} | _] = stacktrace)
       when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
    cond do
      is_list(args) ->
        format_undef(mod, fun, length(args), undef, stacktrace)

      is_integer(args) ->
        format_undef(mod, fun, args, undef, stacktrace)

      true ->
        {format_stop(undef), undef}
    end
  end

  defp maybe_normalize(reason, stacktrace) do
    # If this is already an exception (even an ErlangError), we format it as an
    # exception. Otherwise, we try to normalize it, and if it's normalized as an
    # ErlangError we instead format it as an exit.
    if is_exception(reason) do
      {[?\n | Exception.format_banner(:error, reason, stacktrace)], reason}
    else
      case Exception.normalize(:error, reason, stacktrace) do
        %ErlangError{} ->
          {format_stop(reason), reason}

        exception ->
          {[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
      end
    end
  end

  defp format(kind, payload, stacktrace) do
    [?\n, Exception.format_banner(kind, payload, stacktrace) | format_stacktrace(stacktrace)]
  end

  defp format_stacktrace(stacktrace) do
    for entry <- stacktrace do
      ["\n    " | Exception.format_stacktrace_entry(entry)]
    end
  end

  defp registered_name(name) when is_atom(name), do: [registered_name: name]
  defp registered_name(_name), do: []

  defp format_mfa(mod, fun, :undefined),
    do: [inspect(mod), ?., Code.Identifier.inspect_as_function(fun) | "/?"]

  defp format_mfa(mod, fun, args),
    do: Exception.format_mfa(mod, fun, args)

  defp exit_reason(:exit, reason, stack), do: {reason, stack}
  defp exit_reason(:error, reason, stack), do: {reason, stack}
  defp exit_reason(:throw, value, stack), do: {{:nocatch, value}, stack}

  ## Deprecated helpers

  defp format_from([]), do: ""
  defp format_from([from]), do: " (from #{inspect(from)})"
  defp format_from([from, stacktrace]) when is_list(stacktrace), do: " (from #{inspect(from)})"

  defp format_from([from, node_name]) when is_atom(node_name),
    do: " (from #{inspect(from)} on #{inspect(node_name)})"

  defp format_client([from]) do
    "\nClient #{inspect(from)} is dead"
  end

  defp format_client([from, stacktrace]) when is_list(stacktrace) do
    ["\nClient #{inspect(from)} is alive\n" | format_stacktrace(stacktrace)]
  end

  defp format_client(_) do
    []
  end

  defp format_undef(mod, fun, arity, undef, stacktrace) do
    opts = [module: mod, function: fun, arity: arity, reason: undef]
    exception = UndefinedFunctionError.exception(opts)
    {[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
  end
end

defmodule Logger.Translator do
  @moduledoc """
  Default translation for Erlang log messages.

  Logger allows developers to rewrite log messages provided by
  OTP applications into a format more compatible with Elixir
  log messages by providing a translator.

  A translator is simply a tuple containing a module and a function
  that can be added and removed via the `Logger.add_translator/1` and
  `Logger.remove_translator/1` functions and is invoked for every Erlang
  message above the minimum log level with four arguments:

    * `min_level` - the current Logger level
    * `level` - the level of the message being translated
    * `kind` - if the message is a `:report` or `:format`
    * `message` - the message to format. If it is `:report`, it is a tuple
      with `{report_type, report_data}`, if it is `:format`, it is a
      tuple with `{format_message, format_args}`.

  The function must return:

    * `{:ok, chardata, metadata}` - if the message translation with its metadata
    * `{:ok, chardata}` - the translated message
    * `:skip` - if the message is not meant to be translated nor logged
    * `:none` - if there is no translation, which triggers the next translator

  See the function `translate/4` in this module for an example implementation
  and the default messages translated by Logger.
  """

  @doc """
  Built-in translation function.
  """
  def translate(min_level, level, kind, message)

  def translate(min_level, _level, :report, {:logger, %{label: label} = report}) do
    case label do
      {:gen_server, :terminate} ->
        report_gen_server_terminate(min_level, report)

      {:gen_event, :terminate} ->
        report_gen_event_terminate(min_level, report)

      _ ->
        :skip
    end
  end

  def translate(min_level, _level, :report, {{:proc_lib, :crash}, data}) do
    report_crash(min_level, data)
  end

  def translate(min_level, _level, :report, {{:supervisor, :progress}, data}) do
    report_supervisor_progress(min_level, data)
  end

  def translate(min_level, _level, :report, {{:supervisor, _}, data}) do
    report_supervisor(min_level, data)
  end

  def translate(
        _min_level,
        _level,
        :report,
        {{:application_controller, :progress}, [application: app, started_at: node]}
      ) do
    {:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
  end

  def translate(
        _min_level,
        _level,
        :report,
        {{:application_controller, :exit}, [application: app, exited: reason, type: _type]}
      ) do
    {:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
  end

  def translate(
        _min_level,
        :error,
        :report,
        {{Task.Supervisor, :terminating},
         %{
           name: name,
           starter: starter,
           function: function,
           args: args,
           reason: reason
         }}
      ) do
    opts = Application.get_env(:logger, :translator_inspect_opts)

    {formatted, reason} = format_reason(reason)
    metadata = [crash_reason: reason] ++ registered_name(name)

    msg =
      ["Task #{inspect(name)} started from #{inspect(starter)} terminating"] ++
        [formatted, "\nFunction: #{inspect(function, opts)}"] ++
        ["\n    Args: #{inspect(args, opts)}"]

    {:ok, msg, metadata}
  end

  def translate(min_level, :error, :format, message) do
    case message do
      # This is no longer emitted by Erlang/OTP but it may be
      # manually emitted by libraries like connection.
      # TODO: Remove this translation on Elixir v1.14
      {'** Generic server ' ++ _, [name, last, state, reason | client]} ->
        opts = Application.get_env(:logger, :translator_inspect_opts)
        {formatted, reason} = format_reason(reason)
        metadata = [crash_reason: reason] ++ registered_name(name)

        msg =
          ["GenServer #{inspect(name)} terminating", formatted] ++
            ["\nLast message#{format_from(client)}: #{inspect(last, opts)}"]

        if min_level == :debug do
          msg = [msg, "\nState: #{inspect(state, opts)}" | format_client(client)]
          {:ok, msg, metadata}
        else
          {:ok, msg, metadata}
        end

      {'Error in process ' ++ _, [pid, node, {reason, stack}]} ->
        reason = Exception.normalize(:error, reason, stack)

        msg = [
          "Process ",
          inspect(pid),
          " on node ",
          inspect(node),
          " raised an exception" | format(:error, reason, stack)
        ]

        {:ok, msg, [crash_reason: exit_reason(:error, reason, stack)]}

      {'Error in process ' ++ _, [pid, {reason, stack}]} ->
        reason = Exception.normalize(:error, reason, stack)
        msg = ["Process ", inspect(pid), " raised an exception" | format(:error, reason, stack)]
        {:ok, msg, [crash_reason: exit_reason(:error, reason, stack)]}

      _ ->
        :none
    end
  end

  def translate(_min_level, :info, :report, {
        :std_info,
        [application: app, exited: reason, type: _type]
      }) do
    {:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
  end

  def translate(min_level, :error, :report, {{:error_logger, :error_report}, data}) do
    report_supervisor(min_level, data)
  end

  def translate(min_level, :error, :report, {:supervisor_report, data}) do
    report_supervisor(min_level, data)
  end

  def translate(min_level, :error, :report, {:crash_report, data}) do
    report_crash(min_level, data)
  end

  def translate(min_level, :info, :report, {:progress, [{:supervisor, _} | _] = data}) do
    report_supervisor_progress(min_level, data)
  end

  def translate(_min_level, :info, :report, {:progress, [application: app, started_at: node]}) do
    {:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
  end

  ## Helpers

  def translate(_min_level, _level, _kind, _message) do
    :none
  end

  defp report_gen_server_terminate(min_level, report) do
    inspect_opts = Application.get_env(:logger, :translator_inspect_opts)

    %{
      client_info: client,
      last_message: last,
      name: name,
      reason: reason,
      state: state
    } = report

    {formatted, reason} = format_reason(reason)
    metadata = [crash_reason: reason] ++ registered_name(name)

    msg =
      ["GenServer ", inspect(name), " terminating", formatted] ++
        ["\nLast message", format_last_message_from(client), ": ", inspect(last, inspect_opts)]

    if min_level == :debug do
      msg = [msg, "\nState: ", inspect(state, inspect_opts) | format_client_info(client)]
      {:ok, msg, metadata}
    else
      {:ok, msg, metadata}
    end
  end

  defp report_gen_event_terminate(min_level, report) do
    inspect_opts = Application.get_env(:logger, :translator_inspect_opts)

    %{
      handler: handler,
      last_message: last,
      name: name,
      reason: reason,
      state: state
    } = report

    reason =
      case reason do
        {:EXIT, why} -> why
        _ -> reason
      end

    {formatted, reason} = format_reason(reason)
    metadata = [crash_reason: reason] ++ registered_name(name)

    msg =
      [":gen_event handler ", inspect(handler), " installed in ", inspect(name), " terminating"] ++
        [formatted, "\nLast message: ", inspect(last, inspect_opts)]

    if min_level == :debug do
      {:ok, [msg, "\nState: ", inspect(state, inspect_opts)], metadata}
    else
      {:ok, msg, metadata}
    end
  end

  defp report_supervisor_progress(
         min_level,
         supervisor: sup,
         started: [{:pid, pid}, {:id, id} | started]
       ) do
    msg =
      ["Child ", inspect(id), " of Supervisor ", sup_name(sup), " started"] ++
        ["\nPid: ", inspect(pid)] ++ child_info(min_level, started)

    {:ok, msg}
  end

  defp report_supervisor_progress(
         min_level,
         supervisor: sup,
         started: [{:pid, pid} | started]
       ) do
    msg =
      ["Child of Supervisor ", sup_name(sup), " started", "\nPid: ", inspect(pid)] ++
        child_info(min_level, started)

    {:ok, msg}
  end

  defp report_supervisor_progress(_min_level, _other), do: :none

  defp report_supervisor(
         min_level,
         supervisor: sup,
         errorContext: context,
         reason: reason,
         offender: [{:pid, pid}, {:id, id} | offender]
       ) do
    pid_info =
      if is_pid(pid) and context != :shutdown do
        ["\nPid: ", inspect(pid)]
      else
        []
      end

    msg =
      ["Child ", inspect(id), " of Supervisor ", sup_name(sup)] ++
        [?\s, sup_context(context), "\n** (exit) ", offender_reason(reason, context)] ++
        pid_info ++ child_info(min_level, offender)

    {:ok, msg}
  end

  defp report_supervisor(
         min_level,
         supervisor: sup,
         errorContext: context,
         reason: reason,
         offender: [{:nb_children, n}, {:id, id} | offender]
       ) do
    msg =
      ["Children ", inspect(id), " of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
        ["\n** (exit) ", offender_reason(reason, context), "\nNumber: ", Integer.to_string(n)] ++
        child_info(min_level, offender)

    {:ok, msg}
  end

  defp report_supervisor(
         min_level,
         supervisor: sup,
         errorContext: context,
         reason: reason,
         offender: [{:pid, pid} | offender]
       ) do
    msg =
      ["Child of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
        ["\n** (exit) ", offender_reason(reason, context), "\nPid: ", inspect(pid)] ++
        child_info(min_level, offender)

    {:ok, msg}
  end

  defp report_supervisor(_min_level, _other), do: :none

  # If start call raises reason will be of form {:EXIT, reason}
  defp offender_reason({:EXIT, reason}, :start_error) do
    Exception.format_exit(reason)
  end

  defp offender_reason(reason, _context) do
    Exception.format_exit(reason)
  end

  defp sup_name({:local, name}), do: inspect(name)
  defp sup_name({:global, name}), do: inspect(name)
  defp sup_name({:via, _mod, name}), do: inspect(name)
  defp sup_name({pid, mod}), do: [inspect(pid), " (", inspect(mod), ?)]
  defp sup_name(unknown_name), do: inspect(unknown_name)

  defp sup_context(:start_error), do: "failed to start"
  defp sup_context(:child_terminated), do: "terminated"
  defp sup_context(:shutdown), do: "caused shutdown"
  defp sup_context(:shutdown_error), do: "shut down abnormally"

  defp child_info(min_level, [{:mfargs, {mod, fun, args}} | debug]) do
    ["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
  end

  # Comes from bridge with MFA
  defp child_info(min_level, [{:mfa, {mod, fun, args}} | debug]) do
    ["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
  end

  # Comes from bridge with Mod
  defp child_info(min_level, [{:mod, mod} | debug]) do
    ["\nStart Module: ", inspect(mod) | child_debug(min_level, debug)]
  end

  defp child_info(_min_level, _child) do
    []
  end

  defp child_debug(:debug, restart_type: restart, shutdown: shutdown, child_type: type) do
    ["\nRestart: ", inspect(restart), "\nShutdown: ", inspect(shutdown)] ++
      ["\nType: ", inspect(type)]
  end

  defp child_debug(_min_level, _child) do
    []
  end

  defp report_crash(min_level, [[{:initial_call, initial_call} | crashed], linked]) do
    mfa = initial_call_to_mfa(initial_call)
    report_crash(min_level, crashed, [{:initial_call, mfa}], linked)
  end

  defp report_crash(min_level, [crashed, linked]) do
    report_crash(min_level, crashed, [], linked)
  end

  defp report_crash(min_level, crashed, extra, linked) do
    [
      {:pid, pid},
      {:registered_name, name},
      {:error_info, {kind, reason, stack}} | crashed
    ] = crashed

    dictionary = crashed[:dictionary]
    reason = Exception.normalize(kind, reason, stack)

    case Keyword.get(dictionary, :logger_enabled, true) do
      false ->
        :skip

      true ->
        user_metadata = Keyword.get(dictionary, :"$logger_metadata$", %{}) |> Map.to_list()

        msg =
          ["Process ", crash_name(pid, name), " terminating", format(kind, reason, stack)] ++
            [crash_info(min_level, extra ++ crashed, [?\n]), crash_linked(min_level, linked)]

        extra =
          if ancestors = crashed[:ancestors], do: [{:ancestors, ancestors} | extra], else: extra

        extra =
          if callers = dictionary[:"$callers"], do: [{:callers, callers} | extra], else: extra

        extra = [{:crash_reason, exit_reason(kind, reason, stack)} | extra]
        {:ok, msg, registered_name(name) ++ extra ++ user_metadata}
    end
  end

  defp initial_call_to_mfa({:supervisor, module, _}), do: {module, :init, 1}
  defp initial_call_to_mfa({:supervisor_bridge, module, _}), do: {module, :init, 1}
  defp initial_call_to_mfa({mod, fun, args}) when is_list(args), do: {mod, fun, length(args)}
  defp initial_call_to_mfa(mfa), do: mfa

  defp crash_name(pid, []), do: inspect(pid)
  defp crash_name(pid, name), do: [inspect(name), " (", inspect(pid), ?)]

  defp crash_info(min_level, [{:initial_call, {mod, fun, args}} | info], prefix) do
    [prefix, "Initial Call: ", crash_call(mod, fun, args) | crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level, [{:current_function, {mod, fun, args}} | info], prefix) do
    [prefix, "Current Call: ", crash_call(mod, fun, args) | crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level, [{:current_function, []} | info], prefix) do
    crash_info(min_level, info, prefix)
  end

  defp crash_info(min_level, [{:ancestors, ancestors} | debug], prefix) do
    [prefix, "Ancestors: ", inspect(ancestors) | crash_info(min_level, debug, prefix)]
  end

  defp crash_info(:debug, debug, prefix) do
    for {key, value} <- debug do
      crash_debug(key, value, prefix)
    end
  end

  defp crash_info(_, _, _) do
    []
  end

  defp crash_call(mod, fun, arity) when is_integer(arity) do
    format_mfa(mod, fun, arity)
  end

  defp crash_call(mod, fun, args) do
    format_mfa(mod, fun, length(args))
  end

  defp crash_debug(:current_stacktrace, stack, prefix) do
    stack_prefix = [prefix | "    "]
    stacktrace = Enum.map(stack, &[stack_prefix | Exception.format_stacktrace_entry(&1)])
    [prefix, "Current Stacktrace:" | stacktrace]
  end

  defp crash_debug(key, value, prefix) do
    [prefix, crash_debug_key(key), ?:, ?\s, inspect(value)]
  end

  defp crash_debug_key(key) do
    case key do
      :message_queue_len -> "Message Queue Length"
      :messages -> "Messages"
      :links -> "Links"
      :dictionary -> "Dictionary"
      :trap_exit -> "Trapping Exits"
      :status -> "Status"
      :heap_size -> "Heap Size"
      :stack_size -> "Stack Size"
      :reductions -> "Reductions"
    end
  end

  defp crash_linked(_min_level, []), do: []

  defp crash_linked(min_level, neighbours) do
    Enum.reduce(neighbours, "\nNeighbours:", fn {:neighbour, info}, acc ->
      [acc | crash_neighbour(min_level, info)]
    end)
  end

  @indent "    "

  defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, []} | info]) do
    [?\n, @indent, inspect(pid) | crash_info(min_level, info, [?\n, @indent | @indent])]
  end

  defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, name} | info]) do
    [?\n, @indent, inspect(name), " (", inspect(pid), ")"] ++
      crash_info(min_level, info, [?\n, @indent | @indent])
  end

  defp format_last_message_from({_, {name, _}}), do: [" (from ", inspect(name), ")"]
  defp format_last_message_from({from, _}), do: [" (from ", inspect(from), ")"]
  defp format_last_message_from(_), do: []

  defp format_client_info({from, :dead}),
    do: ["\nClient ", inspect(from), " is dead"]

  defp format_client_info({from, :remote}),
    do: ["\nClient ", inspect(from), " is remote on node ", inspect(node(from))]

  defp format_client_info({_, {name, stacktrace}}),
    do: ["\nClient ", inspect(name), " is alive\n" | format_stacktrace(stacktrace)]

  defp format_client_info(_),
    do: []

  defp format_reason({maybe_exception, [_ | _] = maybe_stacktrace} = reason) do
    try do
      format_stacktrace(maybe_stacktrace)
    catch
      :error, _ ->
        {format_stop(reason), {reason, []}}
    else
      formatted_stacktrace ->
        {formatted, reason} = maybe_normalize(maybe_exception, maybe_stacktrace)
        {[formatted | formatted_stacktrace], {reason, maybe_stacktrace}}
    end
  end

  defp format_reason(reason) do
    {format_stop(reason), {reason, []}}
  end

  defp format_stop(reason) do
    ["\n** (stop) " | Exception.format_exit(reason)]
  end

  # Erlang processes rewrite the :undef error to these reasons when logging
  @gen_undef [:"module could not be loaded", :"function not exported"]

  defp maybe_normalize(undef, [{mod, fun, args, _info} | _] = stacktrace)
       when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
    cond do
      is_list(args) ->
        format_undef(mod, fun, length(args), undef, stacktrace)

      is_integer(args) ->
        format_undef(mod, fun, args, undef, stacktrace)

      true ->
        {format_stop(undef), undef}
    end
  end

  defp maybe_normalize(reason, stacktrace) do
    # If this is already an exception (even an ErlangError), we format it as an
    # exception. Otherwise, we try to normalize it, and if it's normalized as an
    # ErlangError we instead format it as an exit.
    if is_exception(reason) do
      {[?\n | Exception.format_banner(:error, reason, stacktrace)], reason}
    else
      case Exception.normalize(:error, reason, stacktrace) do
        %ErlangError{} ->
          {format_stop(reason), reason}

        exception ->
          {[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
      end
    end
  end

  defp format(kind, payload, stacktrace) do
    [?\n, Exception.format_banner(kind, payload, stacktrace) | format_stacktrace(stacktrace)]
  end

  defp format_stacktrace(stacktrace) do
    for entry <- stacktrace do
      ["\n    " | Exception.format_stacktrace_entry(entry)]
    end
  end

  defp registered_name(name) when is_atom(name), do: [registered_name: name]
  defp registered_name(_name), do: []

  defp format_mfa(mod, fun, :undefined),
    do: [inspect(mod), ?., Code.Identifier.inspect_as_function(fun) | "/?"]

  defp format_mfa(mod, fun, args),
    do: Exception.format_mfa(mod, fun, args)

  defp exit_reason(:exit, reason, stack), do: {reason, stack}
  defp exit_reason(:error, reason, stack), do: {reason, stack}
  defp exit_reason(:throw, value, stack), do: {{:nocatch, value}, stack}

  ## Deprecated helpers

  defp format_from([]), do: ""
  defp format_from([from]), do: " (from #{inspect(from)})"
  defp format_from([from, stacktrace]) when is_list(stacktrace), do: " (from #{inspect(from)})"

  defp format_from([from, node_name]) when is_atom(node_name),
    do: " (from #{inspect(from)} on #{inspect(node_name)})"

  defp format_client([from]) do
    "\nClient #{inspect(from)} is dead"
  end

  defp format_client([from, stacktrace]) when is_list(stacktrace) do
    ["\nClient #{inspect(from)} is alive\n" | format_stacktrace(stacktrace)]
  end

  defp format_client(_) do
    []
  end

  defp format_undef(mod, fun, arity, undef, stacktrace) do
    opts = [module: mod, function: fun, arity: arity, reason: undef]
    exception = UndefinedFunctionError.exception(opts)
    {[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
  end
end


"mix deps.clean": Reword docs to make it consitent with "mix clean"

Use "delete" instead of "remove" just to make it consitent with "mix clean"
$ mix help
mix clean             # Deletes generated application files

defmodule Mix.Tasks.Deps.Clean do
  use Mix.Task

  @shortdoc "Deletes the given dependencies' files"

  @moduledoc """
  Deletes the given dependencies' files, including build artifacts and fetched
  sources.

  Since this is a destructive action, cleaning of dependencies
  can only happen by passing arguments/options:

    * `dep1, dep2` - the name of dependencies to be deleted
    * `--all`      - deletes all dependencies
    * `--unused`   - deletes only unused dependencies (no longer mentioned
      in the `mix.exs` file)
    * `--unlock`   - also unlocks the deleted dependencies

  By default this task works across all environments, unless `--only`
  is given.
  """

  @switches [unlock: :boolean, all: :boolean, only: :string, unused: :boolean]

  @spec run(OptionParser.argv) :: :ok
  def run(args) do
    Mix.Project.get!
    {opts, apps, _} = OptionParser.parse(args, switches: @switches)

    build = Mix.Project.build_path
            |> Path.dirname
            |> Path.join("#{opts[:only] || :*}/lib")
    deps = Mix.Project.deps_path

    cond do
      opts[:all] ->
        checked_deps(build, deps) |> do_clean(build, deps)
      opts[:unused] ->
        checked_deps(build, deps) |> filter_loaded(opts) |> do_clean(build, deps)
      apps != [] ->
        do_clean(apps, build, deps)
      true ->
        Mix.raise "\"mix deps.clean\" expects dependencies as arguments or " <>
                  "a flag indicating which dependencies to clean. " <>
                  "The --all option will clean all dependencies while " <>
                  "the --unused option cleans unused dependencies"
    end

    if opts[:unlock] do
      Mix.Task.run "deps.unlock", args
    else
      :ok
    end
  end

  defp checked_deps(build, deps) do
    for root <- [deps, build],
        path <- Path.wildcard(Path.join(root, "*")),
        File.dir?(path) do
      Path.basename(path)
    end
    |> Enum.uniq()
    |> List.delete(to_string(Mix.Project.config[:app]))
  end

  defp filter_loaded(apps, opts) do
    load_opts = if only = opts[:only], do: [env: :"#{only}"], else: []
    load_deps = Mix.Dep.loaded(load_opts) |> Enum.map(&Atom.to_string(&1.app))
    Enum.reject(apps, &(&1 in load_deps))
  end

  defp do_clean(apps, build, deps) do
    shell = Mix.shell

    Enum.each apps, fn(app) ->
      shell.info "* Cleaning #{app}"

      build
      |> Path.join(to_string(app))
      |> Path.wildcard
      |> Enum.each(&File.rm_rf!/1)

      deps
      |> Path.join(to_string(app))
      |> File.rm_rf!
    end
  end
end

defmodule Mix.Tasks.Deps.Clean do
  use Mix.Task

  @shortdoc "Removes the given dependencies' files"

  @moduledoc """
  Removes the given dependencies' files, including build artifacts and fetched
  sources.

  Since this is a destructive action, cleaning of dependencies
  can only happen by passing arguments/options:

    * `dep1, dep2` - the name of dependencies to be removed
    * `--all` - removes all dependencies
    * `--unused` - removes only unused dependencies (no longer mentioned
      in the `mix.exs` file)
    * `--unlock` - also unlock the removed dependencies

  By default this task works across all environments, unless `--only`
  is given.
  """

  @switches [unlock: :boolean, all: :boolean, only: :string, unused: :boolean]

  @spec run(OptionParser.argv) :: :ok
  def run(args) do
    Mix.Project.get!
    {opts, apps, _} = OptionParser.parse(args, switches: @switches)

    build = Mix.Project.build_path
            |> Path.dirname
            |> Path.join("#{opts[:only] || :*}/lib")
    deps = Mix.Project.deps_path

    cond do
      opts[:all] ->
        checked_deps(build, deps) |> do_clean(build, deps)
      opts[:unused] ->
        checked_deps(build, deps) |> filter_loaded(opts) |> do_clean(build, deps)
      apps != [] ->
        do_clean(apps, build, deps)
      true ->
        Mix.raise "\"mix deps.clean\" expects dependencies as arguments or " <>
                  "a flag indicating which dependencies to clean. " <>
                  "The --all option will clean all dependencies while " <>
                  "the --unused option cleans unused dependencies"
    end

    if opts[:unlock] do
      Mix.Task.run "deps.unlock", args
    else
      :ok
    end
  end

  defp checked_deps(build, deps) do
    for root <- [deps, build],
        path <- Path.wildcard(Path.join(root, "*")),
        File.dir?(path) do
      Path.basename(path)
    end
    |> Enum.uniq()
    |> List.delete(to_string(Mix.Project.config[:app]))
  end

  defp filter_loaded(apps, opts) do
    load_opts = if only = opts[:only], do: [env: :"#{only}"], else: []
    load_deps = Mix.Dep.loaded(load_opts) |> Enum.map(&Atom.to_string(&1.app))
    Enum.reject(apps, &(&1 in load_deps))
  end

  defp do_clean(apps, build, deps) do
    shell = Mix.shell

    Enum.each apps, fn(app) ->
      shell.info "* Cleaning #{app}"

      build
      |> Path.join(to_string(app))
      |> Path.wildcard
      |> Enum.each(&File.rm_rf!/1)

      deps
      |> Path.join(to_string(app))
      |> File.rm_rf!
    end
  end
end


Add a test case to previous commit.

Add a test case to previous commit.

Code.require_file "../test_helper", __FILE__
Code.require_file "../fixtures/default_args", __FILE__

module ModuleTest
  mixin ExUnit::Case

  module A
    def foo
      1 + bar + baz
    end

    def bar
      1
    end

    private

    def baz
      1
    end
  end

  module B
    mixin ModuleTest::A

    def bar
      2
    end

    def baz
      3
    end
  end

  def simple_default_args_test
    1 = DefaultArgs.arity0
    1 = DefaultArgs.arity0(1)
    2 = DefaultArgs.arity0(2)

    { 1, 2 } = DefaultArgs.arity1(1)
    { 1, 2 } = DefaultArgs.arity1(1, 2)
    { 1, 1 } = DefaultArgs.arity1(1, 1)
  end

  def many_default_args_test
    { 1, 2 } = DefaultArgs.many0
    { 1, 2 } = DefaultArgs.many0(1)
    { 2, 2 } = DefaultArgs.many0(2)
    { 1, 2 } = DefaultArgs.many0(1, 2)
    { 2, 3 } = DefaultArgs.many0(2, 3)

    { 0, 1, 2 } = DefaultArgs.many1(0)
    { 0, 1, 2 } = DefaultArgs.many1(0, 1)
    { 0, 2, 2 } = DefaultArgs.many1(0, 2)
    { 0, 1, 2 } = DefaultArgs.many1(0, 1, 2)
    { 0, 2, 3 } = DefaultArgs.many1(0, 2, 3)
  end

  def clash_default_args_test
    [1,2,3]  = DefaultArgs.clash([])
    { 1, 2 } = DefaultArgs.clash()
    { 2, 2 } = DefaultArgs.clash(2)
    { 2, 1 } = DefaultArgs.clash(2, 1)
  end

  def atom_default_args_test
    1 = DefaultArgs.atom('foo)
    2 = DefaultArgs.atom('bar)
  end

  def operators_signature_test
    operators(2, {'atom, 2})
  end

  def send_test
    { 1, 2 } = DefaultArgs.send 'many0
    { 1, 2 } = DefaultArgs.send 'many0, [1]
    { 2, 2 } = DefaultArgs.send 'many0, [2]
    { 1, 2 } = DefaultArgs.send 'many0, [1,2]
    { 2, 3 } = DefaultArgs.send 'many0, [2,3]
  end

  def respond_to_test
    true = OrderedDict.respond_to? 'from_list, 1
    true = OrderedDict.respond_to? '__module_name__, 0
    true = OrderedDict.respond_to? 'respond_to?, 2
    false = OrderedDict.respond_to? 'mixin, 1
    false = OrderedDict.respond_to? 'whatever, 0
    false = OrderedDict.respond_to? '__elixir_exported__, 1
  end

  def mixin_methods_test
    assert_included {'from_list, 1}, OrderedDict.__mixin_methods__
    assert_included {'respond_to?, 2}, OrderedDict.__mixin_methods__
    assert_included {'__module_name__, 0}, OrderedDict.__mixin_methods__
    assert_not_included {'whatever, 0}, OrderedDict.__mixin_methods__
    assert_not_included {'elixir_exported, 1}, OrderedDict.__mixin_methods__
  end

  def local_methods_test
    assert_included {'from_list, 1}, OrderedDict.__local_methods__
    assert_not_included {'respond_to?, 2}, OrderedDict.__local_methods__
    assert_included {'__module_name__, 0}, OrderedDict.__local_methods__
    assert_not_included {'whatever, 0}, OrderedDict.__local_methods__
    assert_not_included {'elixir_exported, 1}, OrderedDict.__local_methods__
  end

  def builtin_not_allowed_test
    assert_error {'builtin_not_allowed, { 'set_ivar, 1 } }, -> 1.set_ivar('foo, 'bar)
  end

  module Example
  end

  def inspect_test
    "ModuleTest::Example" = ModuleTest::Example.inspect

    with_ivars = #ModuleTest::Example().set_ivar('foo, 'bar).set_ivar('bar, 'baz)
    "<#ModuleTest::Example {'bar: 'baz, 'foo: 'bar}>" = with_ivars.inspect
  end

  def to_s_test
    "ModuleTest::Example" = ModuleTest::Example.to_s
    with_ivars = #ModuleTest::Example().set_ivar('foo, 'bar).set_ivar('bar, 'baz)
    "<#ModuleTest::Example {'bar: 'baz, 'foo: 'bar}>" = with_ivars.to_s
  end

  def get_and_set_ivars_test
    dict = { 'a: 1, 'b: 2 }
    ~dict = #ModuleTest::Example().set_ivars(dict).get_ivars
  end

  def update_ivar_test
    [1] = #ModuleTest::Example().set_ivar('foo, []).update_ivar('foo, _.push(1)).get_ivar('foo)
  end

  def remove_ivar_test
    nil = #ModuleTest::Example().set_ivar('foo, 'bar).remove_ivar('foo).get_ivar('foo)
  end

  def local_and_remote_calls_test
    3 = ModuleTest::A.foo
    4 = ModuleTest::B.foo
  end

  def eval_test
    2 = Module.eval("1 + 1")
    3 = Module.eval("foo + 1", 'foo: 2)
    4 = Module.eval(__FILE__, __LINE__, "foo + 1", 'foo: 3)
    5 = Module.eval(__FILE__, __LINE__, "bar + 1", ['foo/3, 'bar/4])
  end

  private

  def operators(1 + 1, 'atom/2)
  end
end

Code.require_file "../test_helper", __FILE__
Code.require_file "../fixtures/default_args", __FILE__

module ModuleTest
  mixin ExUnit::Case

  module A
    def foo
      1 + bar + baz
    end

    def bar
      1
    end

    private

    def baz
      1
    end
  end

  module B
    mixin ModuleTest::A

    def bar
      2
    end

    def baz
      3
    end
  end

  def simple_default_args_test
    1 = DefaultArgs.arity0
    1 = DefaultArgs.arity0(1)
    2 = DefaultArgs.arity0(2)

    { 1, 2 } = DefaultArgs.arity1(1)
    { 1, 2 } = DefaultArgs.arity1(1, 2)
    { 1, 1 } = DefaultArgs.arity1(1, 1)
  end

  def many_default_args_test
    { 1, 2 } = DefaultArgs.many0
    { 1, 2 } = DefaultArgs.many0(1)
    { 2, 2 } = DefaultArgs.many0(2)
    { 1, 2 } = DefaultArgs.many0(1, 2)
    { 2, 3 } = DefaultArgs.many0(2, 3)

    { 0, 1, 2 } = DefaultArgs.many1(0)
    { 0, 1, 2 } = DefaultArgs.many1(0, 1)
    { 0, 2, 2 } = DefaultArgs.many1(0, 2)
    { 0, 1, 2 } = DefaultArgs.many1(0, 1, 2)
    { 0, 2, 3 } = DefaultArgs.many1(0, 2, 3)
  end

  def clash_default_args_test
    [1,2,3]  = DefaultArgs.clash([])
    { 1, 2 } = DefaultArgs.clash()
    { 2, 2 } = DefaultArgs.clash(2)
    { 2, 1 } = DefaultArgs.clash(2, 1)
  end

  def atom_default_args_test
    1 = DefaultArgs.atom('foo)
    2 = DefaultArgs.atom('bar)
  end

  def operators_signature_test
    operators(2, {'atom, 2})
  end

  def send_test
    { 1, 2 } = DefaultArgs.send 'many0
    { 1, 2 } = DefaultArgs.send 'many0, [1]
    { 2, 2 } = DefaultArgs.send 'many0, [2]
    { 1, 2 } = DefaultArgs.send 'many0, [1,2]
    { 2, 3 } = DefaultArgs.send 'many0, [2,3]
  end

  def respond_to_test
    true = OrderedDict.respond_to? 'from_list, 1
    true = OrderedDict.respond_to? '__module_name__, 0
    true = OrderedDict.respond_to? 'respond_to?, 2
    false = OrderedDict.respond_to? 'mixin, 1
    false = OrderedDict.respond_to? 'whatever, 0
    false = OrderedDict.respond_to? '__elixir_exported__, 1
  end

  def mixin_methods_test
    assert_included {'from_list, 1}, OrderedDict.__mixin_methods__
    assert_included {'respond_to?, 2}, OrderedDict.__mixin_methods__
    assert_included {'__module_name__, 0}, OrderedDict.__mixin_methods__
    assert_not_included {'whatever, 0}, OrderedDict.__mixin_methods__
    assert_not_included {'elixir_exported, 1}, OrderedDict.__mixin_methods__
  end

  def local_methods_test
    assert_included {'from_list, 1}, OrderedDict.__local_methods__
    assert_not_included {'respond_to?, 2}, OrderedDict.__local_methods__
    assert_included {'__module_name__, 0}, OrderedDict.__local_methods__
    assert_not_included {'whatever, 0}, OrderedDict.__local_methods__
    assert_not_included {'elixir_exported, 1}, OrderedDict.__local_methods__
  end

  def builtin_not_allowed_test
    assert_error {'builtin_not_allowed, { 'set_ivar, 1 } }, -> 1.set_ivar('foo, 'bar)
  end

  module Example
  end

  def inspect_test
    "ModuleTest::Example" = ModuleTest::Example.inspect

    with_ivars = #ModuleTest::Example().set_ivar('foo, 'bar).set_ivar('bar, 'baz)
    "<#ModuleTest::Example {'bar: 'baz, 'foo: 'bar}>" = with_ivars.inspect
  end

  def to_s_test
    "ModuleTest::Example" = ModuleTest::Example.to_s
    with_ivars = #ModuleTest::Example().set_ivar('foo, 'bar).set_ivar('bar, 'baz)
    "<#ModuleTest::Example {'bar: 'baz, 'foo: 'bar}>" = with_ivars.to_s
  end

  def get_and_set_ivars_test
    dict = { 'a: 1, 'b: 2 }
    ~dict = #ModuleTest::Example().set_ivars(dict).get_ivars
  end

  def update_ivar_test
    [1] = #ModuleTest::Example().set_ivar('foo, []).update_ivar('foo, _.push(1)).get_ivar('foo)
  end

  def remove_ivar_test
    nil = #ModuleTest::Example().set_ivar('foo, 'bar).remove_ivar('foo).get_ivar('foo)
  end

  def local_and_remote_calls_test
    3 = ModuleTest::A.foo
    4 = ModuleTest::B.foo
  end

  def eval_test
    2 = Module.eval("1 + 1")
    3 = Module.eval("foo + 1", 'foo: 2)
    4 = Module.eval(__FILE__, __LINE__, "foo + 1", 'foo: 3)
  end

  private

  def operators(1 + 1, 'atom/2)
  end
end


Fix use of logical OR assignment (fixes #387)

defmodule AsciinemaWeb.AsciicastView do
  use AsciinemaWeb, :view
  import Scrivener.HTML
  alias AsciinemaWeb.Endpoint
  alias AsciinemaWeb.Router.Helpers.Extra, as: RoutesX
  alias AsciinemaWeb.UserView
  import UserView, only: [theme_options: 0]

  def player(src, opts \\ [])

  def player(src, opts) when is_binary(src) do
    if Keyword.get(opts, :player_v3) do
      container_id = Keyword.fetch!(opts, :container_id)

      props =
        [src: src, preload: true]
        |> Keyword.merge(opts)
        |> Ext.Keyword.rename(t: :startAt)
        |> Enum.into(%{})
        |> Map.drop([:container_id, :player_v3])

      content_tag(:script) do
        ~E"""
          window.players = window.players || new Map();
          window.players.set('<%= container_id %>', <%= {:safe, Jason.encode!(props)} %>);
        """
      end
    else
      opts =
        [id: "player", src: src, preload: true]
        |> Keyword.merge(opts)
        |> Ext.Keyword.rename(t: :"start-at", size: :"font-size")

      content_tag(:"asciinema-player", opts, do: [])
    end
  end

  def player(asciicast, opts) do
    opts =
      Keyword.merge(
        [
          cols: asciicast.terminal_columns,
          rows: asciicast.terminal_lines,
          theme: theme_name(asciicast),
          poster: poster(asciicast, Keyword.get(opts, :player_v3)),
          title: title(asciicast),
          author: author_username(asciicast),
          "author-url": author_profile_url(asciicast),
          "author-img-url": author_avatar_url(asciicast)
        ],
        opts
      )

    player(file_url(asciicast), opts)
  end

  @container_vertical_padding 2 * 4
  @approx_char_width 7
  @approx_char_height 16

  def cinema_height(asciicast) do
    ratio =
      asciicast.terminal_lines * @approx_char_height /
        (asciicast.terminal_columns * @approx_char_width)

    round(@container_vertical_padding + 100 * ratio)
  end

  def embed_script(asciicast) do
    src = Routes.asciicast_url(Endpoint, :show, asciicast) <> ".js"
    id = "asciicast-#{Phoenix.Param.to_param(asciicast)}"
    content_tag(:script, [src: src, id: id, async: true], do: [])
  end

  defp file_url(asciicast) do
    RoutesX.asciicast_file_url(asciicast)
  end

  defp asciicast_oembed_url(asciicast, format) do
    Routes.oembed_url(
      Endpoint,
      :show,
      url: Routes.asciicast_url(Endpoint, :show, asciicast),
      format: format
    )
  end

  defp short_text_description(asciicast) do
    if asciicast.description do
      asciicast.description
      |> HtmlSanitizeEx.strip_tags()
      |> String.replace(~r/[\r\n]+/, " ")
      |> truncate(200)
    else
      "Recorded by #{author_username(asciicast)}"
    end
  end

  defp truncate(text, length) do
    if String.length(text) > length do
      String.slice(text, 0, length - 3) <> "..."
    else
      text
    end
  end

  defp alternate_link_type(asciicast) do
    case asciicast.version do
      1 -> "application/asciicast+json"
      2 -> "application/x-asciicast"
      _ -> nil
    end
  end

  def download_filename(asciicast) do
    case asciicast.version do
      1 -> "#{asciicast.id}.json"
      2 -> "#{asciicast.id}.cast"
      _ -> nil
    end
  end

  defp poster(asciicast, true), do: text_poster(asciicast)
  defp poster(asciicast, _), do: base64_poster(asciicast)

  defp base64_poster(asciicast) do
    encoded =
      asciicast
      |> Map.get(:snapshot)
      |> Jason.encode!(escape: :unicode_safe)
      |> Base.encode64()

    "data:application/json;base64," <> encoded
  end

  @csi_init "\x1b["
  @sgr_reset "\x1b[0m"

  defp text_poster(asciicast) do
    text =
      asciicast
      |> Map.get(:snapshot)
      |> Enum.map(&line_to_text/1)
      |> Enum.join("\r\n")

    "data:text/plain," <> text <> @csi_init <> "?25l"
  end

  defp line_to_text(segments) do
    segments
    |> Enum.map(&segment_to_text/1)
    |> Enum.join("")
    |> String.replace(~r/\e\[0m\s*$/, "\e[0m")
  end

  defp segment_to_text([text, attrs]) do
    params =
      attrs
      |> Enum.reject(fn {_k, v} -> v == false end)
      |> sgr_params()

    case params do
      [] ->
        text

      params ->
        @csi_init <> Enum.join(params, ";") <> "m" <> text <> @sgr_reset
    end
  end

  defp sgr_params([{k, v} | rest]) do
    param =
      case {k, v} do
        {"fg", c} when is_number(c) and c < 8 -> "3#{c}"
        {"fg", c} when is_number(c) -> "38;5;#{c}"
        {"fg", "rgb(" <> _} -> "38;2;#{parse_rgb(v)}"
        {"fg", [r, g, b]} -> "38;2;#{r};#{g};#{b}"
        {"bg", c} when is_number(c) and c < 8 -> "4#{c}"
        {"bg", c} when is_number(c) -> "48;5;#{c}"
        {"bg", "rgb(" <> _} -> "48;2;#{parse_rgb(v)}"
        {"bg", [r, g, b]} -> "48;2;#{r};#{g};#{b}"
        {"bold", true} -> "1"
        {"italic", true} -> "3"
        {"underline", true} -> "4"
        {"blink", true} -> "5"
        {"inverse", true} -> "7"
        {"strikethrough", true} -> "9"
      end

    [param | sgr_params(rest)]
  end

  defp sgr_params([]), do: []

  defp parse_rgb("rgb(" <> c) do
    c
    |> String.slice(0, String.length(c) - 1)
    |> String.split(",")
    |> Enum.join(";")
  end

  def description(asciicast) do
    desc = String.trim("#{asciicast.description}")

    if present?(desc) do
      {:safe, HtmlSanitizeEx.basic_html(Earmark.as_html!(desc))}
    end
  end

  def os_info(asciicast) do
    os_from_user_agent(asciicast) || os_from_uname(asciicast)
  end

  defp os_from_user_agent(asciicast) do
    if ua = asciicast.user_agent do
      if match = Regex.run(~r{^asciinema/\d(\.\d+)+ [^/\s]+/[^/\s]+ (.+)$}, ua) do
        [_, _, os] = match

        os
        |> String.replace("-", "/")
        |> String.split("/")
        |> List.first()
        |> String.replace(~r/Darwin/i, "macOS")
      end
    end
  end

  defp os_from_uname(asciicast) do
    if uname = asciicast.uname do
      cond do
        uname =~ ~r/Linux/i -> "Linux"
        uname =~ ~r/Darwin/i -> "macOS"
        true -> uname |> String.split(~r/[\s-]/) |> List.first()
      end
    end
  end

  def shell_info(asciicast) do
    Path.basename("#{asciicast.shell}")
  end

  def term_info(asciicast) do
    asciicast.terminal_type
  end

  def views_count(asciicast) do
    asciicast.views_count
  end

  def active_link(title, active?, opts) do
    opts =
      if active? do
        class = Keyword.get(opts, :class, "") <> " active"
        Keyword.put(opts, :class, class)
      else
        opts
      end

    link(title, opts)
  end

  def nav_link(title, path, active?) do
    content_tag :li, class: "nav-item" do
      active_link(title, active?, to: path, class: "nav-link")
    end
  end

  def title(asciicast) do
    cond do
      present?(asciicast.title) ->
        asciicast.title

      present?(asciicast.command) && asciicast.command != asciicast.shell ->
        asciicast.command

      asciicast.private ->
        "untitled"

      true ->
        "asciicast:#{asciicast.id}"
    end
  end

  def duration(asciicast) do
    if d = asciicast.duration do
      d = round(d)
      minutes = div(d, 60)
      seconds = rem(d, 60)
      :io_lib.format("~2..0B:~2..0B", [minutes, seconds])
    end
  end

  def theme_name(asciicast) do
    asciicast.theme_name || default_theme_name(asciicast)
  end

  def default_theme_name(asciicast) do
    UserView.theme_name(asciicast.user) || "asciinema"
  end

  def author_username(asciicast) do
    UserView.username(asciicast.user)
  end

  def author_avatar_url(asciicast) do
    UserView.avatar_url(asciicast.user)
  end

  def author_profile_path(asciicast) do
    profile_path(asciicast.user)
  end

  def author_profile_url(asciicast) do
    profile_url(asciicast.user)
  end

  def class(%{} = attrs) do
    attrs
    |> Enum.map(&class/1)
    |> Enum.join(" ")
  end

  def class({"fg", fg}) when is_integer(fg), do: "fg-#{fg}"
  def class({"bg", bg}) when is_integer(bg), do: "bg-#{bg}"
  def class({"bold", true}), do: "bright"
  def class({"underline", true}), do: "underline"
  def class(_), do: nil

  def style(attrs) do
    styles =
      []
      |> add_style("color", attrs["fg"])
      |> add_style("background-color", attrs["bg"])

    case styles do
      [] -> nil
      _ -> Enum.join(styles, ";")
    end
  end

  defp add_style(styles, attr, [r, g, b]) do
    ["#{attr}:rgb(#{r},#{g},#{b})" | styles]
  end

  defp add_style(styles, _, _), do: styles

  def thumbnail(asciicast, width \\ 80, height \\ 15) do
    lines = asciicast.snapshot || []

    lines
    |> drop_trailing_blank_lines
    |> fill_to_height(height)
    |> take_last(height)
    |> adjust_colors
    |> split_chunks
    |> crop_to_width(width)
    |> group_chunks
  end

  def drop_trailing_blank_lines(lines) do
    lines
    |> Enum.reverse()
    |> Enum.drop_while(&blank_line?/1)
    |> Enum.reverse()
  end

  def fill_to_height(lines, height) do
    if height - Enum.count(lines) > 0 do
      enums = [lines, Stream.cycle([[]])]

      enums
      |> Stream.concat()
      |> Enum.take(height)
    else
      lines
    end
  end

  def take_last(lines, height) do
    lines
    |> Enum.reverse()
    |> Enum.take(height)
    |> Enum.reverse()
  end

  def blank_line?(line) do
    Enum.all?(line, &blank_chunk?/1)
  end

  def blank_chunk?(["", _attrs]), do: true

  def blank_chunk?([text, attrs]) do
    text = String.trim(text)
    text == "" && attrs == %{}
  end

  def adjust_colors(lines) do
    Enum.map(lines, &adjust_line_colors/1)
  end

  def adjust_line_colors(chunks) do
    Enum.map(chunks, &adjust_chunk_colors/1)
  end

  def adjust_chunk_colors([text, attrs]) do
    {text, adjust_attrs_colors(attrs)}
  end

  def adjust_attrs_colors(attrs) do
    attrs
    |> adjust_fg()
    |> adjust_bg()
    |> invert_colors()
  end

  def adjust_fg(%{"bold" => true, "fg" => fg} = attrs)
      when is_integer(fg) and fg < 8 do
    Map.put(attrs, "fg", fg + 8)
  end

  def adjust_fg(attrs), do: attrs

  def adjust_bg(%{"blink" => true, "bg" => bg} = attrs)
      when is_integer(bg) and bg < 8 do
    Map.put(attrs, "bg", bg + 8)
  end

  def adjust_bg(attrs), do: attrs

  @default_fg_code 7
  @default_bg_code 0

  def invert_colors(%{"inverse" => true} = attrs) do
    fg = attrs["bg"] || @default_bg_code
    bg = attrs["fg"] || @default_fg_code
    Map.merge(attrs, %{"fg" => fg, "bg" => bg})
  end

  def invert_colors(attrs), do: attrs

  def split_chunks(lines) do
    Enum.map(lines, fn line ->
      Enum.flat_map(line, &split_chunk/1)
    end)
  end

  def split_chunk([text, attrs]) do
    split_chunk({text, attrs})
  end

  def split_chunk({text, attrs}) do
    text
    |> String.codepoints()
    |> Enum.map(&{&1, attrs})
  end

  def crop_to_width(lines, width) do
    Enum.map(lines, &crop_line_to_width(&1, width))
  end

  def crop_line_to_width(chunks, width) do
    Enum.take(chunks, width)
  end

  def group_chunks(lines) do
    Enum.map(lines, &group_line_chunks/1)
  end

  def group_line_chunks([]), do: []

  def group_line_chunks([first_chunk | chunks]) do
    {chunks, last_chunk} =
      Enum.reduce(chunks, {[], first_chunk}, fn {text, attrs},
                                                {chunks, {prev_text, prev_attrs}} ->
        if attrs == prev_attrs do
          {chunks, {prev_text <> text, attrs}}
        else
          {[{prev_text, prev_attrs} | chunks], {text, attrs}}
        end
      end)

    Enum.reverse([last_chunk | chunks])
  end

  def render("show.svg", %{asciicast: asciicast} = params) do
    cols = asciicast.terminal_columns
    rows = asciicast.terminal_lines

    lines = adjust_colors(asciicast.snapshot || [])

    bg_lines = add_coords(lines)

    text_lines =
      lines
      |> split_chunks()
      |> add_coords()
      |> remove_blank_chunks()

    render(
      "_terminal.svg",
      cols: cols,
      rows: rows,
      bg_lines: bg_lines,
      text_lines: text_lines,
      rx: params[:rx],
      ry: params[:ry],
      font_family: params[:font_family],
      theme_name: theme_name(asciicast)
    )
  end

  defp add_coords(lines) do
    for {chunks, y} <- Enum.with_index(lines) do
      {_, chunks} =
        Enum.reduce(chunks, {0, []}, fn {text, attrs}, {x, chunks} ->
          width = String.length(text)
          chunk = %{text: text, attrs: attrs, x: x, width: width}
          {x + width, [chunk | chunks]}
        end)

      chunks = Enum.reverse(chunks)

      %{y: y, chunks: chunks}
    end
  end

  defp remove_blank_chunks(lines) do
    for line <- lines do
      chunks = Enum.reject(line.chunks, &(String.trim(&1.text) == ""))
      %{line | chunks: chunks}
    end
  end

  def svg_text_class(%{} = attrs) do
    classes =
      attrs
      |> Enum.reject(fn {attr, _} -> attr == "bg" end)
      |> Enum.map(&svg_text_class/1)
      |> Enum.filter(& &1)

    case classes do
      [] -> nil
      _ -> Enum.join(classes, " ")
    end
  end

  def svg_text_class({"fg", fg}) when is_integer(fg), do: "c-#{fg}"
  def svg_text_class({"bold", true}), do: "br"
  def svg_text_class({"italic", true}), do: "it"
  def svg_text_class({"underline", true}), do: "un"
  def svg_text_class(_), do: nil

  def svg_rect_style(%{"bg" => [_r, _g, _b] = c}), do: "fill:#{hex(c)}"
  def svg_rect_style(_), do: nil

  def svg_rect_class(%{"bg" => bg}) when is_integer(bg), do: "c-#{bg}"
  def svg_rect_class(_), do: nil

  def svg_text_style(attrs) do
    case attrs["fg"] do
      [_r, _g, _b] = c -> "fill:#{hex(c)}"
      _ -> nil
    end
  end

  defp hex([r, g, b]) do
    "##{hex(r)}#{hex(g)}#{hex(b)}"
  end

  defp hex(int) do
    int
    |> Integer.to_string(16)
    |> String.pad_leading(2, "0")
  end

  def percent(float) do
    "#{Decimal.round(Decimal.from_float(float), 3)}%"
  end

  def asciicast_gc_days do
    Asciinema.Asciicasts.gc_days()
  end
end

defmodule AsciinemaWeb.AsciicastView do
  use AsciinemaWeb, :view
  import Scrivener.HTML
  alias AsciinemaWeb.Endpoint
  alias AsciinemaWeb.Router.Helpers.Extra, as: RoutesX
  alias AsciinemaWeb.UserView
  import UserView, only: [theme_options: 0]

  def player(src, opts \\ [])

  def player(src, opts) when is_binary(src) do
    if Keyword.get(opts, :player_v3) do
      container_id = Keyword.fetch!(opts, :container_id)

      props =
        [src: src, preload: true]
        |> Keyword.merge(opts)
        |> Ext.Keyword.rename(t: :startAt)
        |> Enum.into(%{})
        |> Map.drop([:container_id, :player_v3])

      content_tag(:script) do
        ~E"""
          window.players ||= new Map();
          window.players.set('<%= container_id %>', <%= {:safe, Jason.encode!(props)} %>);
        """
      end
    else
      opts =
        [id: "player", src: src, preload: true]
        |> Keyword.merge(opts)
        |> Ext.Keyword.rename(t: :"start-at", size: :"font-size")

      content_tag(:"asciinema-player", opts, do: [])
    end
  end

  def player(asciicast, opts) do
    opts =
      Keyword.merge(
        [
          cols: asciicast.terminal_columns,
          rows: asciicast.terminal_lines,
          theme: theme_name(asciicast),
          poster: poster(asciicast, Keyword.get(opts, :player_v3)),
          title: title(asciicast),
          author: author_username(asciicast),
          "author-url": author_profile_url(asciicast),
          "author-img-url": author_avatar_url(asciicast)
        ],
        opts
      )

    player(file_url(asciicast), opts)
  end

  @container_vertical_padding 2 * 4
  @approx_char_width 7
  @approx_char_height 16

  def cinema_height(asciicast) do
    ratio =
      asciicast.terminal_lines * @approx_char_height /
        (asciicast.terminal_columns * @approx_char_width)

    round(@container_vertical_padding + 100 * ratio)
  end

  def embed_script(asciicast) do
    src = Routes.asciicast_url(Endpoint, :show, asciicast) <> ".js"
    id = "asciicast-#{Phoenix.Param.to_param(asciicast)}"
    content_tag(:script, [src: src, id: id, async: true], do: [])
  end

  defp file_url(asciicast) do
    RoutesX.asciicast_file_url(asciicast)
  end

  defp asciicast_oembed_url(asciicast, format) do
    Routes.oembed_url(
      Endpoint,
      :show,
      url: Routes.asciicast_url(Endpoint, :show, asciicast),
      format: format
    )
  end

  defp short_text_description(asciicast) do
    if asciicast.description do
      asciicast.description
      |> HtmlSanitizeEx.strip_tags()
      |> String.replace(~r/[\r\n]+/, " ")
      |> truncate(200)
    else
      "Recorded by #{author_username(asciicast)}"
    end
  end

  defp truncate(text, length) do
    if String.length(text) > length do
      String.slice(text, 0, length - 3) <> "..."
    else
      text
    end
  end

  defp alternate_link_type(asciicast) do
    case asciicast.version do
      1 -> "application/asciicast+json"
      2 -> "application/x-asciicast"
      _ -> nil
    end
  end

  def download_filename(asciicast) do
    case asciicast.version do
      1 -> "#{asciicast.id}.json"
      2 -> "#{asciicast.id}.cast"
      _ -> nil
    end
  end

  defp poster(asciicast, true), do: text_poster(asciicast)
  defp poster(asciicast, _), do: base64_poster(asciicast)

  defp base64_poster(asciicast) do
    encoded =
      asciicast
      |> Map.get(:snapshot)
      |> Jason.encode!(escape: :unicode_safe)
      |> Base.encode64()

    "data:application/json;base64," <> encoded
  end

  @csi_init "\x1b["
  @sgr_reset "\x1b[0m"

  defp text_poster(asciicast) do
    text =
      asciicast
      |> Map.get(:snapshot)
      |> Enum.map(&line_to_text/1)
      |> Enum.join("\r\n")

    "data:text/plain," <> text <> @csi_init <> "?25l"
  end

  defp line_to_text(segments) do
    segments
    |> Enum.map(&segment_to_text/1)
    |> Enum.join("")
    |> String.replace(~r/\e\[0m\s*$/, "\e[0m")
  end

  defp segment_to_text([text, attrs]) do
    params =
      attrs
      |> Enum.reject(fn {_k, v} -> v == false end)
      |> sgr_params()

    case params do
      [] ->
        text

      params ->
        @csi_init <> Enum.join(params, ";") <> "m" <> text <> @sgr_reset
    end
  end

  defp sgr_params([{k, v} | rest]) do
    param =
      case {k, v} do
        {"fg", c} when is_number(c) and c < 8 -> "3#{c}"
        {"fg", c} when is_number(c) -> "38;5;#{c}"
        {"fg", "rgb(" <> _} -> "38;2;#{parse_rgb(v)}"
        {"fg", [r, g, b]} -> "38;2;#{r};#{g};#{b}"
        {"bg", c} when is_number(c) and c < 8 -> "4#{c}"
        {"bg", c} when is_number(c) -> "48;5;#{c}"
        {"bg", "rgb(" <> _} -> "48;2;#{parse_rgb(v)}"
        {"bg", [r, g, b]} -> "48;2;#{r};#{g};#{b}"
        {"bold", true} -> "1"
        {"italic", true} -> "3"
        {"underline", true} -> "4"
        {"blink", true} -> "5"
        {"inverse", true} -> "7"
        {"strikethrough", true} -> "9"
      end

    [param | sgr_params(rest)]
  end

  defp sgr_params([]), do: []

  defp parse_rgb("rgb(" <> c) do
    c
    |> String.slice(0, String.length(c) - 1)
    |> String.split(",")
    |> Enum.join(";")
  end

  def description(asciicast) do
    desc = String.trim("#{asciicast.description}")

    if present?(desc) do
      {:safe, HtmlSanitizeEx.basic_html(Earmark.as_html!(desc))}
    end
  end

  def os_info(asciicast) do
    os_from_user_agent(asciicast) || os_from_uname(asciicast)
  end

  defp os_from_user_agent(asciicast) do
    if ua = asciicast.user_agent do
      if match = Regex.run(~r{^asciinema/\d(\.\d+)+ [^/\s]+/[^/\s]+ (.+)$}, ua) do
        [_, _, os] = match

        os
        |> String.replace("-", "/")
        |> String.split("/")
        |> List.first()
        |> String.replace(~r/Darwin/i, "macOS")
      end
    end
  end

  defp os_from_uname(asciicast) do
    if uname = asciicast.uname do
      cond do
        uname =~ ~r/Linux/i -> "Linux"
        uname =~ ~r/Darwin/i -> "macOS"
        true -> uname |> String.split(~r/[\s-]/) |> List.first()
      end
    end
  end

  def shell_info(asciicast) do
    Path.basename("#{asciicast.shell}")
  end

  def term_info(asciicast) do
    asciicast.terminal_type
  end

  def views_count(asciicast) do
    asciicast.views_count
  end

  def active_link(title, active?, opts) do
    opts =
      if active? do
        class = Keyword.get(opts, :class, "") <> " active"
        Keyword.put(opts, :class, class)
      else
        opts
      end

    link(title, opts)
  end

  def nav_link(title, path, active?) do
    content_tag :li, class: "nav-item" do
      active_link(title, active?, to: path, class: "nav-link")
    end
  end

  def title(asciicast) do
    cond do
      present?(asciicast.title) ->
        asciicast.title

      present?(asciicast.command) && asciicast.command != asciicast.shell ->
        asciicast.command

      asciicast.private ->
        "untitled"

      true ->
        "asciicast:#{asciicast.id}"
    end
  end

  def duration(asciicast) do
    if d = asciicast.duration do
      d = round(d)
      minutes = div(d, 60)
      seconds = rem(d, 60)
      :io_lib.format("~2..0B:~2..0B", [minutes, seconds])
    end
  end

  def theme_name(asciicast) do
    asciicast.theme_name || default_theme_name(asciicast)
  end

  def default_theme_name(asciicast) do
    UserView.theme_name(asciicast.user) || "asciinema"
  end

  def author_username(asciicast) do
    UserView.username(asciicast.user)
  end

  def author_avatar_url(asciicast) do
    UserView.avatar_url(asciicast.user)
  end

  def author_profile_path(asciicast) do
    profile_path(asciicast.user)
  end

  def author_profile_url(asciicast) do
    profile_url(asciicast.user)
  end

  def class(%{} = attrs) do
    attrs
    |> Enum.map(&class/1)
    |> Enum.join(" ")
  end

  def class({"fg", fg}) when is_integer(fg), do: "fg-#{fg}"
  def class({"bg", bg}) when is_integer(bg), do: "bg-#{bg}"
  def class({"bold", true}), do: "bright"
  def class({"underline", true}), do: "underline"
  def class(_), do: nil

  def style(attrs) do
    styles =
      []
      |> add_style("color", attrs["fg"])
      |> add_style("background-color", attrs["bg"])

    case styles do
      [] -> nil
      _ -> Enum.join(styles, ";")
    end
  end

  defp add_style(styles, attr, [r, g, b]) do
    ["#{attr}:rgb(#{r},#{g},#{b})" | styles]
  end

  defp add_style(styles, _, _), do: styles

  def thumbnail(asciicast, width \\ 80, height \\ 15) do
    lines = asciicast.snapshot || []

    lines
    |> drop_trailing_blank_lines
    |> fill_to_height(height)
    |> take_last(height)
    |> adjust_colors
    |> split_chunks
    |> crop_to_width(width)
    |> group_chunks
  end

  def drop_trailing_blank_lines(lines) do
    lines
    |> Enum.reverse()
    |> Enum.drop_while(&blank_line?/1)
    |> Enum.reverse()
  end

  def fill_to_height(lines, height) do
    if height - Enum.count(lines) > 0 do
      enums = [lines, Stream.cycle([[]])]

      enums
      |> Stream.concat()
      |> Enum.take(height)
    else
      lines
    end
  end

  def take_last(lines, height) do
    lines
    |> Enum.reverse()
    |> Enum.take(height)
    |> Enum.reverse()
  end

  def blank_line?(line) do
    Enum.all?(line, &blank_chunk?/1)
  end

  def blank_chunk?(["", _attrs]), do: true

  def blank_chunk?([text, attrs]) do
    text = String.trim(text)
    text == "" && attrs == %{}
  end

  def adjust_colors(lines) do
    Enum.map(lines, &adjust_line_colors/1)
  end

  def adjust_line_colors(chunks) do
    Enum.map(chunks, &adjust_chunk_colors/1)
  end

  def adjust_chunk_colors([text, attrs]) do
    {text, adjust_attrs_colors(attrs)}
  end

  def adjust_attrs_colors(attrs) do
    attrs
    |> adjust_fg()
    |> adjust_bg()
    |> invert_colors()
  end

  def adjust_fg(%{"bold" => true, "fg" => fg} = attrs)
      when is_integer(fg) and fg < 8 do
    Map.put(attrs, "fg", fg + 8)
  end

  def adjust_fg(attrs), do: attrs

  def adjust_bg(%{"blink" => true, "bg" => bg} = attrs)
      when is_integer(bg) and bg < 8 do
    Map.put(attrs, "bg", bg + 8)
  end

  def adjust_bg(attrs), do: attrs

  @default_fg_code 7
  @default_bg_code 0

  def invert_colors(%{"inverse" => true} = attrs) do
    fg = attrs["bg"] || @default_bg_code
    bg = attrs["fg"] || @default_fg_code
    Map.merge(attrs, %{"fg" => fg, "bg" => bg})
  end

  def invert_colors(attrs), do: attrs

  def split_chunks(lines) do
    Enum.map(lines, fn line ->
      Enum.flat_map(line, &split_chunk/1)
    end)
  end

  def split_chunk([text, attrs]) do
    split_chunk({text, attrs})
  end

  def split_chunk({text, attrs}) do
    text
    |> String.codepoints()
    |> Enum.map(&{&1, attrs})
  end

  def crop_to_width(lines, width) do
    Enum.map(lines, &crop_line_to_width(&1, width))
  end

  def crop_line_to_width(chunks, width) do
    Enum.take(chunks, width)
  end

  def group_chunks(lines) do
    Enum.map(lines, &group_line_chunks/1)
  end

  def group_line_chunks([]), do: []

  def group_line_chunks([first_chunk | chunks]) do
    {chunks, last_chunk} =
      Enum.reduce(chunks, {[], first_chunk}, fn {text, attrs},
                                                {chunks, {prev_text, prev_attrs}} ->
        if attrs == prev_attrs do
          {chunks, {prev_text <> text, attrs}}
        else
          {[{prev_text, prev_attrs} | chunks], {text, attrs}}
        end
      end)

    Enum.reverse([last_chunk | chunks])
  end

  def render("show.svg", %{asciicast: asciicast} = params) do
    cols = asciicast.terminal_columns
    rows = asciicast.terminal_lines

    lines = adjust_colors(asciicast.snapshot || [])

    bg_lines = add_coords(lines)

    text_lines =
      lines
      |> split_chunks()
      |> add_coords()
      |> remove_blank_chunks()

    render(
      "_terminal.svg",
      cols: cols,
      rows: rows,
      bg_lines: bg_lines,
      text_lines: text_lines,
      rx: params[:rx],
      ry: params[:ry],
      font_family: params[:font_family],
      theme_name: theme_name(asciicast)
    )
  end

  defp add_coords(lines) do
    for {chunks, y} <- Enum.with_index(lines) do
      {_, chunks} =
        Enum.reduce(chunks, {0, []}, fn {text, attrs}, {x, chunks} ->
          width = String.length(text)
          chunk = %{text: text, attrs: attrs, x: x, width: width}
          {x + width, [chunk | chunks]}
        end)

      chunks = Enum.reverse(chunks)

      %{y: y, chunks: chunks}
    end
  end

  defp remove_blank_chunks(lines) do
    for line <- lines do
      chunks = Enum.reject(line.chunks, &(String.trim(&1.text) == ""))
      %{line | chunks: chunks}
    end
  end

  def svg_text_class(%{} = attrs) do
    classes =
      attrs
      |> Enum.reject(fn {attr, _} -> attr == "bg" end)
      |> Enum.map(&svg_text_class/1)
      |> Enum.filter(& &1)

    case classes do
      [] -> nil
      _ -> Enum.join(classes, " ")
    end
  end

  def svg_text_class({"fg", fg}) when is_integer(fg), do: "c-#{fg}"
  def svg_text_class({"bold", true}), do: "br"
  def svg_text_class({"italic", true}), do: "it"
  def svg_text_class({"underline", true}), do: "un"
  def svg_text_class(_), do: nil

  def svg_rect_style(%{"bg" => [_r, _g, _b] = c}), do: "fill:#{hex(c)}"
  def svg_rect_style(_), do: nil

  def svg_rect_class(%{"bg" => bg}) when is_integer(bg), do: "c-#{bg}"
  def svg_rect_class(_), do: nil

  def svg_text_style(attrs) do
    case attrs["fg"] do
      [_r, _g, _b] = c -> "fill:#{hex(c)}"
      _ -> nil
    end
  end

  defp hex([r, g, b]) do
    "##{hex(r)}#{hex(g)}#{hex(b)}"
  end

  defp hex(int) do
    int
    |> Integer.to_string(16)
    |> String.pad_leading(2, "0")
  end

  def percent(float) do
    "#{Decimal.round(Decimal.from_float(float), 3)}%"
  end

  def asciicast_gc_days do
    Asciinema.Asciicasts.gc_days()
  end
end


fix image upload bug

fix image upload bug

defmodule FarmbotExt.API do
  alias FarmbotExt.{API, JWT}

  alias FarmbotCore.JSON

  alias FarmbotCore.Asset.{
    FbosConfig,
    FirmwareConfig,
    StorageAuth
  }

  alias FarmbotCore.{BotState, BotState.JobProgress.Percent, Project}

  require FarmbotCore.Logger
  import FarmbotCore.Config, only: [get_config_value: 3]

  use Tesla
  alias Tesla.Multipart

  @version Project.version()
  @target Project.target()

  # adapter(Tesla.Adapter.Hackney)
  plug(Tesla.Middleware.JSON, decode: &JSON.decode/1, encode: &JSON.encode/1)
  plug(Tesla.Middleware.FollowRedirects)
  # plug(Tesla.Middleware.Logger)

  def client do
    binary_token = get_config_value(:string, "authorization", "token")
    server = get_config_value(:string, "authorization", "server")
    {:ok, _tkn} = JWT.decode(binary_token)

    uri = URI.parse(server)
    url = (uri.scheme || "https") <> "://" <> uri.host <> ":" <> to_string(uri.port)
    user_agent = "FarmbotOS/#{@version} (#{@target}) #{@target} ()"

    Tesla.client([
      {Tesla.Middleware.BaseUrl, url},
      {Tesla.Middleware.Headers,
       [
         {"content-type", "application/json"},
         {"authorization", "Bearer: " <> binary_token},
         {"user-agent", user_agent}
       ]}
    ])
  end

  def storage_client(%StorageAuth{url: url}) do
    server = get_config_value(:string, "authorization", "server")
    uri = URI.parse(server)
    user_agent = "FarmbotOS/#{@version} (#{@target}) #{@target} ()"

    Tesla.client([
      {Tesla.Middleware.BaseUrl, "#{uri.scheme}:#{url}"},
      {Tesla.Middleware.Headers,
       [
         {"user-agent", user_agent}
       ]},
      {Tesla.Middleware.FormUrlencoded, []}
    ])
  end

  @file_chunk 4096
  @progress_steps 50

  def upload_image(image_filename, meta \\ %{}) do
    {:ok, changeset} = API.get_changeset(StorageAuth)
    storage_auth = %StorageAuth{form_data: form_data} = Ecto.Changeset.apply_changes(changeset)

    content_length = :filelib.file_size(image_filename)
    {:ok, pid} = Agent.start_link(fn -> 0 end)

    prog = %Percent{
      status: "working",
      percent: 0,
      file_type: Path.extname(image_filename),
      type: :image,
      time: DateTime.utc_now()
    }

    stream =
      image_filename
      |> File.stream!([], @file_chunk)
      |> Stream.each(fn chunk ->
        Agent.update(pid, fn sent ->
          size = sent + byte_size(chunk)
          prog = put_progress(prog, size, content_length)
          BotState.set_job_progress(image_filename, prog)
          size
        end)
      end)

    opts =
      if String.contains?(storage_auth.url, "direct_upload") do
        []
      else
        [filename: image_filename, headers: [{"Content-Type", "image/jpeg"}]]
      end

    mp =
      Multipart.new()
      |> Multipart.add_field("key", form_data.key)
      |> Multipart.add_field("acl", form_data.acl)
      |> Multipart.add_field("policy", form_data.policy)
      |> Multipart.add_field("signature", form_data.signature)
      |> Multipart.add_field("Content-Type", form_data."Content-Type")
      |> Multipart.add_field("GoogleAccessId", form_data."GoogleAccessId")
      |> Multipart.add_field("file", stream, opts)

    storage_resp =
      storage_auth
      |> API.storage_client()
      |> API.request(
        method: String.to_existing_atom(String.downcase(storage_auth.verb)),
        body: mp
      )

    with {:ok, %{url: url, status: s}} when s > 199 and s < 300 <- storage_resp,
         attachment_url <- Path.join(url, form_data.key),
         client <- API.client(),
         body <- %{attachment_url: attachment_url, meta: meta},
         {:ok, %{status: s}} = r when s > 199 and s < 300 <- API.post(client, "/api/images", body) do
      BotState.set_job_progress(image_filename, %{prog | status: "complete", percent: 100})
      r
    else
      {:ok, %{status: s, body: body}} when s > 399 ->
        FarmbotCore.Logger.error(1, "Failed to upload image (HTTP: #{s}): #{inspect(body)}")
        BotState.set_job_progress(image_filename, %{prog | percent: -1, status: "error"})
        {:error, body}

      er ->
        FarmbotCore.Logger.error(1, "Failed to upload image: #{inspect(er)}")
        BotState.set_job_progress(image_filename, %{prog | percent: -1, status: "error"})
        er
    end
  end

  def put_progress(prog, size, max) do
    fraction = size / max
    completed = trunc(fraction * @progress_steps)
    percent = trunc(fraction * 100)
    unfilled = @progress_steps - completed

    IO.write(
      :stderr,
      "\r|#{String.duplicate("=", completed)}#{String.duplicate(" ", unfilled)}| #{percent}% (#{
        bytes_to_mb(size)
      } / #{bytes_to_mb(max)}) MB"
    )

    status = if percent == 100, do: "complete", else: "working"

    %Percent{
      prog
      | status: status,
        percent: percent
    }
  end

  defp bytes_to_mb(bytes) do
    trunc(bytes / 1024 / 1024)
  end

  @doc "helper for `GET`ing a path."
  def get_body!(path) do
    case API.get(API.client(), path) do
      {:ok, %{body: body, status: 200}} ->
        {:ok, body}

      {:ok, %{body: error, status: status}} when is_binary(error) ->
        get_body_error(path, status, error)

      {:ok, %{body: %{"error" => error}, status: status}} when is_binary(error) ->
        get_body_error(path, status, error)

      {:ok, %{body: error, status: status}} when is_binary(error) ->
        get_body_error(path, status, inspect(error))

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp get_body_error(path, status, error) when is_binary(error) do
    msg = """
    HTTP Error getting: #{path}
    Status Code = #{status}

    #{error}
    """

    {:error, msg}
  end

  @callback get_changeset(module) :: {:ok, %Ecto.Changeset{}} | {:error, term()}
  @callback get_changeset(data :: module | map(), Path.t()) ::
              {:ok, %Ecto.Changeset{}} | {:error, term()}

  @doc "helper for `GET`ing api resources."
  def get_changeset(module) when is_atom(module) do
    get_changeset(struct(module))
  end

  def get_changeset(%module{} = data) do
    get_body!(module.path())
    |> case do
      {:ok, %{} = single} ->
        {:ok, module.changeset(data, single)}

      {:ok, many} when is_list(many) ->
        {:ok, Enum.map(many, &module.changeset(data, &1))}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc "helper for `GET`ing api resources."
  def get_changeset(asset, path)

  # Hacks for dealing with these resources not having #show
  def get_changeset(FbosConfig, _),
    do: get_changeset(FbosConfig)

  def get_changeset(FirmwareConfig, _),
    do: get_changeset(FirmwareConfig)

  def get_changeset(%FbosConfig{} = data, _),
    do: get_changeset(data)

  def get_changeset(%FirmwareConfig{} = data, _),
    do: get_changeset(data)

  def get_changeset(module, path) when is_atom(module) do
    get_changeset(struct(module), path)
  end

  def get_changeset(%module{} = data, path) do
    get_body!(Path.join(module.path(), to_string(path)))
    |> case do
      {:ok, %{} = single} ->
        {:ok, module.changeset(data, single)}

      {:ok, many} when is_list(many) ->
        {:ok, Enum.map(many, &module.changeset(data, &1))}

      {:error, reason} ->
        {:error, reason}
    end
  end
end

defmodule FarmbotExt.API do
  alias FarmbotExt.{API, JWT}

  alias FarmbotCore.JSON

  alias FarmbotCore.Asset.{
    FbosConfig,
    FirmwareConfig,
    StorageAuth
  }

  alias FarmbotCore.{BotState, BotState.JobProgress.Percent, Project}

  require FarmbotCore.Logger
  import FarmbotCore.Config, only: [get_config_value: 3]

  use Tesla
  alias Tesla.Multipart

  @version Project.version()
  @target Project.target()

  # adapter(Tesla.Adapter.Hackney)
  plug(Tesla.Middleware.JSON, decode: &JSON.decode/1, encode: &JSON.encode/1)
  plug(Tesla.Middleware.FollowRedirects)
  # plug(Tesla.Middleware.Logger)

  def client do
    binary_token = get_config_value(:string, "authorization", "token")
    server = get_config_value(:string, "authorization", "server")
    {:ok, _tkn} = JWT.decode(binary_token)

    uri = URI.parse(server)
    url = (uri.scheme || "https") <> "://" <> uri.host <> ":" <> to_string(uri.port)
    user_agent = "FarmbotOS/#{@version} (#{@target}) #{@target} ()"

    Tesla.client([
      {Tesla.Middleware.BaseUrl, url},
      {Tesla.Middleware.Headers,
       [
         {"content-type", "application/json"},
         {"authorization", "Bearer: " <> binary_token},
         {"user-agent", user_agent}
       ]}
    ])
  end

  def storage_client(%StorageAuth{url: url}) do
    server = get_config_value(:string, "authorization", "server")
    uri = URI.parse(server)
    user_agent = "FarmbotOS/#{@version} (#{@target}) #{@target} ()"

    Tesla.client([
      {Tesla.Middleware.BaseUrl, "#{uri.scheme}:#{url}"},
      {Tesla.Middleware.Headers,
       [
         {"user-agent", user_agent}
       ]},
      {Tesla.Middleware.FormUrlencoded, []}
    ])
  end

  @file_chunk 4096
  @progress_steps 50

  def upload_image(image_filename, meta \\ %{}) do
    {:ok, changeset} = API.get_changeset(StorageAuth)
    storage_auth = %StorageAuth{form_data: form_data} = Ecto.Changeset.apply_changes(changeset)

    content_length = :filelib.file_size(image_filename)
    {:ok, pid} = Agent.start_link(fn -> 0 end)

    prog = %Percent{
      status: "working",
      percent: 0,
      file_type: Path.extname(image_filename),
      type: :image,
      time: DateTime.utc_now()
    }

    stream =
      image_filename
      |> File.stream!([], @file_chunk)
      |> Stream.each(fn chunk ->
        Agent.update(pid, fn sent ->
          size = sent + byte_size(chunk)
          prog = put_progress(prog, size, content_length)
          BotState.set_job_progress(image_filename, prog)
          size
        end)
      end)

    mp =
      Multipart.new()
      |> Multipart.add_field("key", form_data.key)
      |> Multipart.add_field("acl", form_data.acl)
      |> Multipart.add_field("policy", form_data.policy)
      |> Multipart.add_field("signature", form_data.signature)
      |> Multipart.add_field("Content-Type", form_data."Content-Type")
      |> Multipart.add_field("GoogleAccessId", form_data."GoogleAccessId")
      |> Multipart.add_field("file", stream)

    storage_resp =
      storage_auth
      |> API.storage_client()
      |> API.request(
        method: String.to_existing_atom(String.downcase(storage_auth.verb)),
        body: mp
      )

    with {:ok, %{url: url, status: s}} when s > 199 and s < 300 <- storage_resp,
         attachment_url <- Path.join(url, form_data.key),
         client <- API.client(),
         body <- %{attachment_url: attachment_url, meta: meta},
         {:ok, %{status: s}} = r when s > 199 and s < 300 <- API.post(client, "/api/images", body) do
      BotState.set_job_progress(image_filename, %{prog | status: "complete", percent: 100})
      r
    else
      {:ok, %{status: s, body: body}} when s > 399 ->
        FarmbotCore.Logger.error(1, "Failed to upload image (HTTP: #{s}): #{inspect(body)}")
        BotState.set_job_progress(image_filename, %{prog | percent: -1, status: "error"})
        {:error, body}

      er ->
        FarmbotCore.Logger.error(1, "Failed to upload image: #{inspect(er)}")
        BotState.set_job_progress(image_filename, %{prog | percent: -1, status: "error"})
        er
    end
  end

  def put_progress(prog, size, max) do
    fraction = size / max
    completed = trunc(fraction * @progress_steps)
    percent = trunc(fraction * 100)
    unfilled = @progress_steps - completed

    IO.write(
      :stderr,
      "\r|#{String.duplicate("=", completed)}#{String.duplicate(" ", unfilled)}| #{percent}% (#{
        bytes_to_mb(size)
      } / #{bytes_to_mb(max)}) MB"
    )

    status = if percent == 100, do: "complete", else: "working"

    %Percent{
      prog
      | status: status,
        percent: percent
    }
  end

  defp bytes_to_mb(bytes) do
    trunc(bytes / 1024 / 1024)
  end

  @doc "helper for `GET`ing a path."
  def get_body!(path) do
    case API.get(API.client(), path) do
      {:ok, %{body: body, status: 200}} ->
        {:ok, body}

      {:ok, %{body: error, status: status}} when is_binary(error) ->
        get_body_error(path, status, error)

      {:ok, %{body: %{"error" => error}, status: status}} when is_binary(error) ->
        get_body_error(path, status, error)

      {:ok, %{body: error, status: status}} when is_binary(error) ->
        get_body_error(path, status, inspect(error))

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp get_body_error(path, status, error) when is_binary(error) do
    msg = """
    HTTP Error getting: #{path}
    Status Code = #{status}

    #{error}
    """

    {:error, msg}
  end

  @callback get_changeset(module) :: {:ok, %Ecto.Changeset{}} | {:error, term()}
  @callback get_changeset(data :: module | map(), Path.t()) ::
              {:ok, %Ecto.Changeset{}} | {:error, term()}

  @doc "helper for `GET`ing api resources."
  def get_changeset(module) when is_atom(module) do
    get_changeset(struct(module))
  end

  def get_changeset(%module{} = data) do
    get_body!(module.path())
    |> case do
      {:ok, %{} = single} ->
        {:ok, module.changeset(data, single)}

      {:ok, many} when is_list(many) ->
        {:ok, Enum.map(many, &module.changeset(data, &1))}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc "helper for `GET`ing api resources."
  def get_changeset(asset, path)

  # Hacks for dealing with these resources not having #show
  def get_changeset(FbosConfig, _),
    do: get_changeset(FbosConfig)

  def get_changeset(FirmwareConfig, _),
    do: get_changeset(FirmwareConfig)

  def get_changeset(%FbosConfig{} = data, _),
    do: get_changeset(data)

  def get_changeset(%FirmwareConfig{} = data, _),
    do: get_changeset(data)

  def get_changeset(module, path) when is_atom(module) do
    get_changeset(struct(module), path)
  end

  def get_changeset(%module{} = data, path) do
    get_body!(Path.join(module.path(), to_string(path)))
    |> case do
      {:ok, %{} = single} ->
        {:ok, module.changeset(data, single)}

      {:ok, many} when is_list(many) ->
        {:ok, Enum.map(many, &module.changeset(data, &1))}

      {:error, reason} ->
        {:error, reason}
    end
  end
end