change logger times

change logger times

defmodule Logger.Backends.FarmbotLogger do
  @moduledoc """
    Logger backend for logging to the frontend and dumping to the API.
    Takes messages that were logged useing Logger, if they can be
    jsonified, adds them too a buffer, when that buffer hits a certain
    size, it tries to dump the messages onto the API.
  """
  alias Farmbot.Sync
  alias Farmbot.HTTP
  use GenEvent
  require Logger
  use Farmbot.DebugLog
  @save_path Application.get_env(:farmbot, :path) <> "/logs.txt"

  # ten megs. i promise
  @max_file_size 1.0e+7
  @filtered "[FILTERED]"

  @typedoc """
    The state of the logger
  """
  @type state :: %{logs: [log_message], posting: boolean}

  @typedoc """
    Type of message for ticker
  """
  @type rpc_log_type
    :: :success
     | :busy
     | :warn
     | :error
     | :info
     | :fun

  @typedoc """
    Elixir Logger level type
  """
  @type logger_level
    :: :info
     | :debug
     | :warn
     | :error

  @typedoc """
    One day this will me more
  """
  @type channel :: :toast

  @type log_message
  :: %{message: String.t,
       channels: [channel],
       created_at: integer,
       meta: %{type: rpc_log_type}}

  def init(_), do: {:ok, %{logs: [], posting: false}}

  # The example said ignore messages for other nodes, so im ignoring messages
  # for other nodes.
  def handle_event({_, gl, {Logger, _, _, _}}, state) when node(gl) != node() do
    {:ok, state}
  end

  # The logger event.
  def handle_event({level, _, {Logger, message, timestamp, metadata}}, state) do
    # if there is type key in the meta we need that to have priority
    type = Keyword.get(metadata, :type, level)
    channels = Keyword.get(metadata, :channels, [])
    created_at = parse_created_at(timestamp)
    san_m = sanitize(message, metadata)
    log = build_log(san_m, created_at, type, channels)
    :ok = GenServer.cast(Farmbot.Transport, {:log, log})
    logs = [log | state.logs]
    if !state.posting and Enum.count(logs) >= 50 do
      # If not already posting, and more than 50 messages
      spawn fn ->
        filterd_logs = Enum.filter(logs, fn(log) ->
          log.message != @filtered
        end)
        do_post(filterd_logs)
      end
      {:ok, %{state | logs: logs, posting: true}}
    else
      {:ok, %{state | logs: logs}}
    end
  end

  def handle_event(:flush, _state), do: {:ok, %{logs: [], posting: false}}

  def handle_call(:post_success, state) do
    debug_log "Logs uploaded!"
    write_file(Enum.reverse(state.logs))
    {:ok, :ok, %{state | posting: false, logs: []}}
  end

  def handle_call(:post_fail, state) do
    debug_log "Logs failed to upload!"
    {:ok, :ok, %{state | posting: false}}
  end

  def handle_call(:messages, state) do
    {:ok, Enum.reverse(state.logs), state}
  end

  def handle_call(:get_state, state), do: {:ok, state, state}

  @spec do_post([log_message]) :: no_return
  defp do_post(logs) do
    try do
      HTTP.post!("/api/logs", Poison.encode!(logs))
      GenEvent.call(Elixir.Logger, __MODULE__, :post_success)
    rescue
      _ -> GenEvent.call(Elixir.Logger, __MODULE__, :post_fail)
    end
  end

  @spec write_file([log_message]) :: no_return
  defp write_file(logs) do
    debug_log("Writing log file!")
    old = read_file()
    new_file = Enum.reduce(logs, old, fn(log, acc) ->
      if log.message != @filtered do
        acc <> log.message <> "\r\n"
      else
        acc
      end
    end)

    bin = fifo(new_file)

    Farmbot.System.FS.transaction fn ->
      File.write(@save_path, bin <> "\r\n")
    end
  end

  # reads the current file.
  # if the file isnt found, returns an empty string
  @spec read_file :: binary
  defp read_file do
    case File.read(@save_path) do
      {:ok, bin} -> bin
      _ -> ""
    end
  end

  # if the file is to big, fifo it
  # trim lines off the file until it is smaller than @max_file_size
  @spec fifo(binary) :: binary
  defp fifo(new_file) when byte_size(new_file) > @max_file_size do
    [_ | rest] = String.split(new_file, "\r\n")
    fifo(Enum.join(rest, "\r\n"))
  end

  defp fifo(new_file), do: new_file

  # if this backend crashes just pop it out of the logger backends.
  # if we don't do this it bacomes a huge mess because of Logger
  # trying to restart this module
  # then this module dying again
  # then printing a HUGE supervisor report
  # then Logger trying to add it again  etc
  def terminate(_,_), do: spawn fn -> Logger.remove_backend(__MODULE__) end

  @spec sanitize(binary, [any]) :: String.t
  defp sanitize(message, meta) do
    module = Keyword.get(meta, :module)
    unless meta[:nopub] do
      message
      |> filter_module(module)
      |> filter_text()
    end
  end

  @modules [
    :"Elixir.Nerves.InterimWiFi",
    :"Elixir.Nerves.NetworkInterface",
    :"Elixir.Nerves.InterimWiFi.WiFiManager.EventHandler",
    :"Elixir.Nerves.InterimWiFi.WiFiManager",
    :"Elixir.Nerves.InterimWiFi.DHCPManager",
    :"Elixir.Nerves.InterimWiFi.Udhcpc",
    :"Elixir.Nerves.NetworkInterface.Worker",
    :"Elixir.Nerves.InterimWiFi.DHCPManager.EventHandler",
    :"Elixir.Nerves.WpaSupplicant",
    :"Elixir.Farmbot.System.NervesCommon.EventManager",
  ]

  for module <- @modules, do: defp filter_module(_, unquote(module)), do: @filtered
  defp filter_module(message, _module), do: message

  defp filter_text(">>" <> m), do: filter_text("#{Sync.device_name()}" <> m)
  defp filter_text(m) when is_binary(m) do
    try do
      Poison.encode!(m)
      m
    rescue
      _ -> @filtered
    end
  end
  defp filter_text(_message), do: @filtered

  # Couuld probably do this inline but wheres the fun in that. its a functional
  # language isn't it?
  # Takes Loggers time stamp and converts it into a unix timestamp.
  defp parse_created_at({{year, month, day}, {hour, minute, second, _mil}}) do
    %DateTime{year: year,
      month: month,
      day: day,
      hour: hour,
      minute: minute,
      second: second,
      microsecond: {0,0},
      std_offset: 0,
      time_zone: "Etc/UTC",
      utc_offset: 0,
      zone_abbr: "UTC"}
    |> DateTime.to_unix#(:milliseconds)
  end

  @spec build_log(String.t, number, rpc_log_type, [channel]) :: log_message
  defp build_log(message, created_at, type, channels) do
    %{message: message,
      created_at: created_at,
      channels: channels,
      meta: %{type: type, x: 0, y: 0, z: 0}}
  end
end

defmodule Logger.Backends.FarmbotLogger do
  @moduledoc """
    Logger backend for logging to the frontend and dumping to the API.
    Takes messages that were logged useing Logger, if they can be
    jsonified, adds them too a buffer, when that buffer hits a certain
    size, it tries to dump the messages onto the API.
  """
  alias Farmbot.Sync
  alias Farmbot.HTTP
  use GenEvent
  require Logger
  use Farmbot.DebugLog
  @save_path Application.get_env(:farmbot, :path) <> "/logs.txt"

  # ten megs. i promise
  @max_file_size 1.0e+7
  @filtered "[FILTERED]"

  @typedoc """
    The state of the logger
  """
  @type state :: %{logs: [log_message], posting: boolean}

  @typedoc """
    Type of message for ticker
  """
  @type rpc_log_type
    :: :success
     | :busy
     | :warn
     | :error
     | :info
     | :fun

  @typedoc """
    Elixir Logger level type
  """
  @type logger_level
    :: :info
     | :debug
     | :warn
     | :error

  @typedoc """
    One day this will me more
  """
  @type channel :: :toast

  @type log_message
  :: %{message: String.t,
       channels: [channel],
       created_at: integer,
       meta: %{type: rpc_log_type}}

  def init(_), do: {:ok, %{logs: [], posting: false}}

  # The example said ignore messages for other nodes, so im ignoring messages
  # for other nodes.
  def handle_event({_, gl, {Logger, _, _, _}}, state) when node(gl) != node() do
    {:ok, state}
  end

  # The logger event.
  def handle_event({level, _, {Logger, message, timestamp, metadata}}, state) do
    # if there is type key in the meta we need that to have priority
    type = Keyword.get(metadata, :type, level)
    channels = Keyword.get(metadata, :channels, [])
    created_at = parse_created_at(timestamp)
    san_m = sanitize(message, metadata)
    log = build_log(san_m, created_at, type, channels)
    :ok = GenServer.cast(Farmbot.Transport, {:log, log})
    logs = [log | state.logs]
    if !state.posting and Enum.count(logs) >= 50 do
      # If not already posting, and more than 50 messages
      spawn fn ->
        filterd_logs = Enum.filter(logs, fn(log) ->
          log.message != @filtered
        end)
        do_post(filterd_logs)
      end
      {:ok, %{state | logs: logs, posting: true}}
    else
      {:ok, %{state | logs: logs}}
    end
  end

  def handle_event(:flush, _state), do: {:ok, %{logs: [], posting: false}}

  def handle_call(:post_success, state) do
    debug_log "Logs uploaded!"
    write_file(Enum.reverse(state.logs))
    {:ok, :ok, %{state | posting: false, logs: []}}
  end

  def handle_call(:post_fail, state) do
    debug_log "Logs failed to upload!"
    {:ok, :ok, %{state | posting: false}}
  end

  def handle_call(:messages, state) do
    {:ok, Enum.reverse(state.logs), state}
  end

  def handle_call(:get_state, state), do: {:ok, state, state}

  @spec do_post([log_message]) :: no_return
  defp do_post(logs) do
    try do
      HTTP.post!("/api/logs", Poison.encode!(logs))
      GenEvent.call(Elixir.Logger, __MODULE__, :post_success)
    rescue
      _ -> GenEvent.call(Elixir.Logger, __MODULE__, :post_fail)
    end
  end

  @spec write_file([log_message]) :: no_return
  defp write_file(logs) do
    debug_log("Writing log file!")
    old = read_file()
    new_file = Enum.reduce(logs, old, fn(log, acc) ->
      if log.message != @filtered do
        acc <> log.message <> "\r\n"
      else
        acc
      end
    end)

    bin = fifo(new_file)

    Farmbot.System.FS.transaction fn ->
      File.write(@save_path, bin <> "\r\n")
    end
  end

  # reads the current file.
  # if the file isnt found, returns an empty string
  @spec read_file :: binary
  defp read_file do
    case File.read(@save_path) do
      {:ok, bin} -> bin
      _ -> ""
    end
  end

  # if the file is to big, fifo it
  # trim lines off the file until it is smaller than @max_file_size
  @spec fifo(binary) :: binary
  defp fifo(new_file) when byte_size(new_file) > @max_file_size do
    [_ | rest] = String.split(new_file, "\r\n")
    fifo(Enum.join(rest, "\r\n"))
  end

  defp fifo(new_file), do: new_file

  # if this backend crashes just pop it out of the logger backends.
  # if we don't do this it bacomes a huge mess because of Logger
  # trying to restart this module
  # then this module dying again
  # then printing a HUGE supervisor report
  # then Logger trying to add it again  etc
  def terminate(_,_), do: spawn fn -> Logger.remove_backend(__MODULE__) end

  @spec sanitize(binary, [any]) :: String.t
  defp sanitize(message, meta) do
    module = Keyword.get(meta, :module)
    unless meta[:nopub] do
      message
      |> filter_module(module)
      |> filter_text()
    end
  end

  @modules [
    :"Elixir.Nerves.InterimWiFi",
    :"Elixir.Nerves.NetworkInterface",
    :"Elixir.Nerves.InterimWiFi.WiFiManager.EventHandler",
    :"Elixir.Nerves.InterimWiFi.WiFiManager",
    :"Elixir.Nerves.InterimWiFi.DHCPManager",
    :"Elixir.Nerves.InterimWiFi.Udhcpc",
    :"Elixir.Nerves.NetworkInterface.Worker",
    :"Elixir.Nerves.InterimWiFi.DHCPManager.EventHandler",
    :"Elixir.Nerves.WpaSupplicant",
    :"Elixir.Farmbot.System.NervesCommon.EventManager",
  ]

  for module <- @modules, do: defp filter_module(_, unquote(module)), do: @filtered
  defp filter_module(message, _module), do: message

  defp filter_text(">>" <> m), do: filter_text("#{Sync.device_name()}" <> m)
  defp filter_text(m) when is_binary(m) do
    try do
      Poison.encode!(m)
      m
    rescue
      _ -> @filtered
    end
  end
  defp filter_text(_message), do: @filtered

  # Couuld probably do this inline but wheres the fun in that. its a functional
  # language isn't it?
  # Takes Loggers time stamp and converts it into a unix timestamp.
  defp parse_created_at({{year, month, day}, {hour, minute, second, _mil}}) do
    %DateTime{year: year,
      month: month,
      day: day,
      hour: hour,
      minute: minute,
      second: second,
      microsecond: {0,0},
      std_offset: 0,
      time_zone: "Etc/UTC",
      utc_offset: 0,
      zone_abbr: "UTC"}
    |> DateTime.to_iso8601
  end

  @spec build_log(String.t, number, rpc_log_type, [channel]) :: log_message
  defp build_log(message, created_at, type, channels) do
    %{message: message,
      created_at: created_at,
      channels: channels,
      meta: %{type: type, x: 0, y: 0, z: 0}}
  end
end


Strip name from checksum file (#11561)

Strip name from checksum file (#11561)

Currently, the notify.exs will generate the checksum to:

```
  * Docs.zip - sha1sum - 3812e2db7c71a6a8b03b75d06ca6debb46aebc55  Docs.zip

  * Docs.zip - sha256sum - cafe86adc01fcc700c83f353aedf746eeffd3b6e1542cedff18af64956ae539b  Docs.zip

  * elixir-otp-23.zip - sha1sum - 35f999b98cbfa5888fefbeafaaf73f661a903bb7  elixir-otp-23.zip

  * elixir-otp-23.zip - sha256sum - 11f970adc3ddc759f349b0e00b6aecd06a1d460f21488d2ffe8f6738f2518728  elixir-otp-23.zip

  * elixir-otp-24.zip - sha1sum - c4224ce1b78247d7c5e04c7cd4576496fd49f002  elixir-otp-24.zip

  * elixir-otp-24.zip - sha256sum - 677fe7a147a40e5ee876eeb7bb8e4b968d2a8a9c42793561b61a08269ca91c19  elixir-otp-24.zip
```

The result have a file name and newline at the end of each line. This change strip
that, so the result will be:

```
  * Docs.zip - sha1sum - 3812e2db7c71a6a8b03b75d06ca6debb46aebc55
  * Docs.zip - sha256sum - cafe86adc01fcc700c83f353aedf746eeffd3b6e1542cedff18af64956ae539b
  * elixir-otp-23.zip - sha1sum - 35f999b98cbfa5888fefbeafaaf73f661a903bb7
  * elixir-otp-23.zip - sha256sum - 11f970adc3ddc759f349b0e00b6aecd06a1d460f21488d2ffe8f6738f2518728
  * elixir-otp-24.zip - sha1sum - c4224ce1b78247d7c5e04c7cd4576496fd49f002
  * elixir-otp-24.zip - sha256sum - 677fe7a147a40e5ee876eeb7bb8e4b968d2a8a9c42793561b61a08269ca91c19
```

# #!/usr/bin/env elixir
[tag] = System.argv()

Mix.install([
  {:req, "~> 0.2.1"},
  {:jason, "~> 1.0"}
])

%{status: 200, body: body} =
  Req.get!("https://api.github.com/repos/elixir-lang/elixir/releases/tags/#{tag}")

if body["draft"] do
  raise "cannot notify a draft release"
end

## Notify on elixir-lang-ann

names_and_checksums =
  for asset <- body["assets"],
      name = asset["name"],
      name =~ ~r/.sha\d+sum$/,
      do: {name, Req.get!(asset["browser_download_url"]).body}

line_items =
  for {name, checksum_and_name} <- Enum.sort(names_and_checksums) do
    [checksum | _] = String.split(checksum_and_name, " ")
    root = Path.rootname(name)
    "." <> type = Path.extname(name)
    "  * #{root} - #{type} - #{checksum}\n"
  end

headers = %{
  "X-Postmark-Server-Token" => System.fetch_env!("ELIXIR_LANG_ANN_TOKEN")
}

body = %{
  "From" => "jose.valim@dashbit.co",
  "To" => "elixir-lang-ann@googlegroups.com",
  "Subject" => "Elixir #{tag} released",
  "HtmlBody" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{line_items}",
  "MessageStream": "outbound"
}

resp = Req.post!("https://api.postmarkapp.com/email", {:json, body}, headers: headers)
IO.puts("#{resp.status} elixir-lang-ann\n#{inspect(resp.body)}")

## Notify on Elixir Forum

headers = %{
  "api-key" => System.fetch_env!("ELIXIR_FORUM_TOKEN"),
  "api-username" => "Elixir"
}

body = %{
  "title" => "Elixir #{tag} released",
  "raw" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{body["body"]}",
  # Elixir News
  "category" => 28
}

resp = Req.post!("https://elixirforum.com/posts.json", {:json, body}, headers: headers)
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")

# #!/usr/bin/env elixir
[tag] = System.argv()

Mix.install([
  {:req, "~> 0.2.1"},
  {:jason, "~> 1.0"}
])

%{status: 200, body: body} =
  Req.get!("https://api.github.com/repos/elixir-lang/elixir/releases/tags/#{tag}")

if body["draft"] do
  raise "cannot notify a draft release"
end

## Notify on elixir-lang-ann

names_and_checksums =
  for asset <- body["assets"],
      name = asset["name"],
      name =~ ~r/.sha\d+sum$/,
      do: {name, Req.get!(asset["browser_download_url"]).body}

line_items =
  for {name, checksum} <- Enum.sort(names_and_checksums) do
    root = Path.rootname(name)
    "." <> type = Path.extname(name)
    "  * #{root} - #{type} - #{checksum}\n"
  end

headers = %{
  "X-Postmark-Server-Token" => System.fetch_env!("ELIXIR_LANG_ANN_TOKEN")
}

body = %{
  "From" => "jose.valim@dashbit.co",
  "To" => "elixir-lang-ann@googlegroups.com",
  "Subject" => "Elixir #{tag} released",
  "HtmlBody" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{line_items}",
  "MessageStream": "outbound"
}

resp = Req.post!("https://api.postmarkapp.com/email", {:json, body}, headers: headers)
IO.puts("#{resp.status} elixir-lang-ann\n#{inspect(resp.body)}")

## Notify on Elixir Forum

headers = %{
  "api-key" => System.fetch_env!("ELIXIR_FORUM_TOKEN"),
  "api-username" => "Elixir"
}

body = %{
  "title" => "Elixir #{tag} released",
  "raw" => "https://github.com/elixir-lang/elixir/releases/tag/#{tag}\n\n#{body["body"]}",
  # Elixir News
  "category" => 28
}

resp = Req.post!("https://elixirforum.com/posts.json", {:json, body}, headers: headers)
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")


deliver/1 spec (#352)

deliver/1 spec (#352)

Change prepared `@spec deliver(any()) :: no_return()` because function ignore parameters (now only `Bamboo.Email.t` required).

defmodule Bamboo.Mailer do
  @moduledoc """
  Sets up mailers that make it easy to configure and swap adapters.

  Adds `deliver_now/1` and `deliver_later/1` functions to the mailer module it is used by.

  ## Bamboo ships with the following adapters

  * `Bamboo.MandrillAdapter`
  * `Bamboo.LocalAdapter`
  * `Bamboo.TestAdapter`
  * or create your own with `Bamboo.Adapter`

  ## Example

      # In your config/config.exs file
      config :my_app, MyApp.Mailer,
        adapter: Bamboo.MandrillAdapter,
        api_key: "my_api_key"

      # Somewhere in your application. Maybe lib/my_app/mailer.ex
      defmodule MyApp.Mailer do
        # Adds deliver_now/1 and deliver_later/1
        use Bamboo.Mailer, otp_app: :my_app
      end

      # Set up your emails
      defmodule MyApp.Email do
        import Bamboo.Email

        def welcome_email do
          new_mail(
            to: "foo@example.com",
            from: "me@example.com",
            subject: "Welcome!!!",
            html_body: "WELCOME",
            text_body: "WELCOME"
          )
        end
      end

      # In a Phoenix controller or some other module
      defmodule MyApp.Foo do
        alias MyApp.Email
        alias MyApp.Mailer

        def register_user do
          # Create a user and whatever else is needed
          # Could also have called Mailer.deliver_later
          Email.welcome_email |> Mailer.deliver_now
        end
      end
  """

  @cannot_call_directly_error """
  cannot call Bamboo.Mailer directly. Instead implement your own Mailer module
  with: use Bamboo.Mailer, otp_app: :my_app
  """

  require Logger
  alias Bamboo.Formatter

  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do

      @spec deliver_now(Bamboo.Email.t) :: Bamboo.Email.t
      def deliver_now(email) do
        config = build_config()
        Bamboo.Mailer.deliver_now(config.adapter, email, config)
      end

      @spec deliver_later(Bamboo.Email.t) :: Bamboo.Email.t
      def deliver_later(email) do
        config = build_config()
        Bamboo.Mailer.deliver_later(config.adapter, email, config)
      end

      otp_app = Keyword.fetch!(opts, :otp_app)

      defp build_config, do: Bamboo.Mailer.build_config(__MODULE__, unquote(otp_app))

      @spec deliver(any()) :: no_return()
      def deliver(_email) do
        raise """
        you called deliver/1, but it has been renamed to deliver_now/1 to add clarity.

        Use deliver_now/1 to send right away, or deliver_later/1 to send in the background.
        """
      end
    end
  end

  @doc """
  Deliver an email right away

  Call your mailer with `deliver_now/1` to send an email right away. Call
  `deliver_later/1` if you want to send in the background to speed things up.
  """
  def deliver_now(_email) do
    raise @cannot_call_directly_error
  end

  @doc """
  Deliver an email in the background

  Call your mailer with `deliver_later/1` to send an email using the configured
  `deliver_later_strategy`. If no `deliver_later_strategy` is set,
  `Bamboo.TaskSupervisorStrategy` will be used. See
  `Bamboo.DeliverLaterStrategy` to learn how to change how emails are delivered
  with `deliver_later/1`.
  """
  def deliver_later(_email) do
    raise @cannot_call_directly_error
  end

  @doc false
  def deliver_now(adapter, email, config) do
    email = email |> validate_and_normalize(adapter)

    if email.to == [] && email.cc == [] && email.bcc == [] do
      debug_unsent(email)
    else
      debug_sent(email, adapter)
      adapter.deliver(email, config)
    end
    email
  end

  @doc false
  def deliver_later(adapter, email, config) do
    email = email |> validate_and_normalize(adapter)

    if email.to == [] && email.cc == [] && email.bcc == [] do
      debug_unsent(email)
    else
      debug_sent(email, adapter)
      config.deliver_later_strategy.deliver_later(adapter, email, config)
    end
    email
  end

  defp debug_sent(email, adapter) do
    Logger.debug """
    Sending email with #{inspect adapter}:

    #{inspect email, limit: :infinity}
    """
  end

  defp debug_unsent(email) do
    Logger.debug """
    Email was not sent because recipients are empty.

    Full email - #{inspect email, limit: :infinity}
    """
  end

  defp validate_and_normalize(email, adapter) do
    email |> validate(adapter) |> normalize_addresses
  end

  defp validate(email, adapter) do
    email
    |> validate_from_address
    |> validate_recipients
    |> validate_attachment_support(adapter)
  end

  defp validate_attachment_support(%{attachments: []} = email, _adapter), do: email
  defp validate_attachment_support(email, adapter) do
    if function_exported?(adapter, :supports_attachments?, 0) && adapter.supports_attachments? do
      email
    else
      raise "the #{adapter} does not support attachments yet."
    end
  end

  defp validate_from_address(%{from: nil}) do
    raise Bamboo.EmptyFromAddressError, nil
  end
  defp validate_from_address(%{from: {_, nil}}) do
    raise Bamboo.EmptyFromAddressError, nil
  end
  defp validate_from_address(email), do: email

  defp validate_recipients(%Bamboo.Email{} = email) do
    if Enum.all?(
      Enum.map([:to, :cc, :bcc], &Map.get(email, &1)),
      &is_nil_recipient?/1
    ) do
      raise Bamboo.NilRecipientsError, email
    else
      email
    end
  end

  defp is_nil_recipient?(nil), do: true
  defp is_nil_recipient?({_, nil}), do: true
  defp is_nil_recipient?([]), do: false
  defp is_nil_recipient?([_|_] = recipients) do
    Enum.all?(recipients, &is_nil_recipient?/1)
  end
  defp is_nil_recipient?(_), do: false

  @doc """
  Wraps to, cc and bcc addresses in a list and normalizes email addresses.

  Also formats the from address. Email normalization/formatting is done by the
  [Bamboo.Formatter] protocol.
  """
  def normalize_addresses(email) do
    %{email |
      from: format(email.from, :from),
      to: format(List.wrap(email.to), :to),
      cc: format(List.wrap(email.cc), :cc),
      bcc: format(List.wrap(email.bcc), :bcc)
    }
  end

  defp format(record, type) do
    Formatter.format_email_address(record, %{type: type})
  end

  @doc false
  def parse_opts(mailer, opts) do
    Logger.warn("#{__MODULE__}.parse_opts/2 has been deprecated. Use #{__MODULE__}.build_config/2")

    otp_app = Keyword.fetch!(opts, :otp_app)
    build_config(mailer, otp_app)
  end

  def build_config(mailer, otp_app) do
    otp_app
    |> Application.fetch_env!(mailer)
    |> Map.new
    |> handle_adapter_config
  end

  defp handle_adapter_config(base_config = %{adapter: adapter}) do
    adapter.handle_config(base_config)
    |> Map.put_new(:deliver_later_strategy, Bamboo.TaskSupervisorStrategy)
  end
end

defmodule Bamboo.Mailer do
  @moduledoc """
  Sets up mailers that make it easy to configure and swap adapters.

  Adds `deliver_now/1` and `deliver_later/1` functions to the mailer module it is used by.

  ## Bamboo ships with the following adapters

  * `Bamboo.MandrillAdapter`
  * `Bamboo.LocalAdapter`
  * `Bamboo.TestAdapter`
  * or create your own with `Bamboo.Adapter`

  ## Example

      # In your config/config.exs file
      config :my_app, MyApp.Mailer,
        adapter: Bamboo.MandrillAdapter,
        api_key: "my_api_key"

      # Somewhere in your application. Maybe lib/my_app/mailer.ex
      defmodule MyApp.Mailer do
        # Adds deliver_now/1 and deliver_later/1
        use Bamboo.Mailer, otp_app: :my_app
      end

      # Set up your emails
      defmodule MyApp.Email do
        import Bamboo.Email

        def welcome_email do
          new_mail(
            to: "foo@example.com",
            from: "me@example.com",
            subject: "Welcome!!!",
            html_body: "WELCOME",
            text_body: "WELCOME"
          )
        end
      end

      # In a Phoenix controller or some other module
      defmodule MyApp.Foo do
        alias MyApp.Email
        alias MyApp.Mailer

        def register_user do
          # Create a user and whatever else is needed
          # Could also have called Mailer.deliver_later
          Email.welcome_email |> Mailer.deliver_now
        end
      end
  """

  @cannot_call_directly_error """
  cannot call Bamboo.Mailer directly. Instead implement your own Mailer module
  with: use Bamboo.Mailer, otp_app: :my_app
  """

  require Logger
  alias Bamboo.Formatter

  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do

      @spec deliver_now(Bamboo.Email.t) :: Bamboo.Email.t
      def deliver_now(email) do
        config = build_config()
        Bamboo.Mailer.deliver_now(config.adapter, email, config)
      end

      @spec deliver_later(Bamboo.Email.t) :: Bamboo.Email.t
      def deliver_later(email) do
        config = build_config()
        Bamboo.Mailer.deliver_later(config.adapter, email, config)
      end

      otp_app = Keyword.fetch!(opts, :otp_app)

      defp build_config, do: Bamboo.Mailer.build_config(__MODULE__, unquote(otp_app))

      @spec deliver(Bamboo.Email.t) :: no_return()
      def deliver(_email) do
        raise """
        you called deliver/1, but it has been renamed to deliver_now/1 to add clarity.

        Use deliver_now/1 to send right away, or deliver_later/1 to send in the background.
        """
      end
    end
  end

  @doc """
  Deliver an email right away

  Call your mailer with `deliver_now/1` to send an email right away. Call
  `deliver_later/1` if you want to send in the background to speed things up.
  """
  def deliver_now(_email) do
    raise @cannot_call_directly_error
  end

  @doc """
  Deliver an email in the background

  Call your mailer with `deliver_later/1` to send an email using the configured
  `deliver_later_strategy`. If no `deliver_later_strategy` is set,
  `Bamboo.TaskSupervisorStrategy` will be used. See
  `Bamboo.DeliverLaterStrategy` to learn how to change how emails are delivered
  with `deliver_later/1`.
  """
  def deliver_later(_email) do
    raise @cannot_call_directly_error
  end

  @doc false
  def deliver_now(adapter, email, config) do
    email = email |> validate_and_normalize(adapter)

    if email.to == [] && email.cc == [] && email.bcc == [] do
      debug_unsent(email)
    else
      debug_sent(email, adapter)
      adapter.deliver(email, config)
    end
    email
  end

  @doc false
  def deliver_later(adapter, email, config) do
    email = email |> validate_and_normalize(adapter)

    if email.to == [] && email.cc == [] && email.bcc == [] do
      debug_unsent(email)
    else
      debug_sent(email, adapter)
      config.deliver_later_strategy.deliver_later(adapter, email, config)
    end
    email
  end

  defp debug_sent(email, adapter) do
    Logger.debug """
    Sending email with #{inspect adapter}:

    #{inspect email, limit: :infinity}
    """
  end

  defp debug_unsent(email) do
    Logger.debug """
    Email was not sent because recipients are empty.

    Full email - #{inspect email, limit: :infinity}
    """
  end

  defp validate_and_normalize(email, adapter) do
    email |> validate(adapter) |> normalize_addresses
  end

  defp validate(email, adapter) do
    email
    |> validate_from_address
    |> validate_recipients
    |> validate_attachment_support(adapter)
  end

  defp validate_attachment_support(%{attachments: []} = email, _adapter), do: email
  defp validate_attachment_support(email, adapter) do
    if function_exported?(adapter, :supports_attachments?, 0) && adapter.supports_attachments? do
      email
    else
      raise "the #{adapter} does not support attachments yet."
    end
  end

  defp validate_from_address(%{from: nil}) do
    raise Bamboo.EmptyFromAddressError, nil
  end
  defp validate_from_address(%{from: {_, nil}}) do
    raise Bamboo.EmptyFromAddressError, nil
  end
  defp validate_from_address(email), do: email

  defp validate_recipients(%Bamboo.Email{} = email) do
    if Enum.all?(
      Enum.map([:to, :cc, :bcc], &Map.get(email, &1)),
      &is_nil_recipient?/1
    ) do
      raise Bamboo.NilRecipientsError, email
    else
      email
    end
  end

  defp is_nil_recipient?(nil), do: true
  defp is_nil_recipient?({_, nil}), do: true
  defp is_nil_recipient?([]), do: false
  defp is_nil_recipient?([_|_] = recipients) do
    Enum.all?(recipients, &is_nil_recipient?/1)
  end
  defp is_nil_recipient?(_), do: false

  @doc """
  Wraps to, cc and bcc addresses in a list and normalizes email addresses.

  Also formats the from address. Email normalization/formatting is done by the
  [Bamboo.Formatter] protocol.
  """
  def normalize_addresses(email) do
    %{email |
      from: format(email.from, :from),
      to: format(List.wrap(email.to), :to),
      cc: format(List.wrap(email.cc), :cc),
      bcc: format(List.wrap(email.bcc), :bcc)
    }
  end

  defp format(record, type) do
    Formatter.format_email_address(record, %{type: type})
  end

  @doc false
  def parse_opts(mailer, opts) do
    Logger.warn("#{__MODULE__}.parse_opts/2 has been deprecated. Use #{__MODULE__}.build_config/2")

    otp_app = Keyword.fetch!(opts, :otp_app)
    build_config(mailer, otp_app)
  end

  def build_config(mailer, otp_app) do
    otp_app
    |> Application.fetch_env!(mailer)
    |> Map.new
    |> handle_adapter_config
  end

  defp handle_adapter_config(base_config = %{adapter: adapter}) do
    adapter.handle_config(base_config)
    |> Map.put_new(:deliver_later_strategy, Bamboo.TaskSupervisorStrategy)
  end
end


Fix test to match updated Door#show

Fix test to match updated Door#show

defmodule Gatekeeper.DoorControllerTest do
  use Gatekeeper.ConnCase

  import Gatekeeper.Factory

  alias Gatekeeper.Door
  alias Gatekeeper.DoorGroup

  @valid_attrs %{name: "some content"}
  @invalid_attrs %{}

  setup do
    conn = conn()
    {:ok, conn: conn}
  end

  test "lists all entries on index", %{conn: conn} do
    conn = get conn, door_path(conn, :index)
    assert html_response(conn, 200) =~ "Listing doors"
  end

  test "renders form for new resources", %{conn: conn} do
    conn = get conn, door_path(conn, :new)
    assert html_response(conn, 200) =~ "New door"
  end

  test "creates resource and redirects when data is valid", %{conn: conn} do
    conn = post conn, door_path(conn, :create), door: @valid_attrs
    assert redirected_to(conn) == door_path(conn, :index)
    assert Repo.get_by(Door, @valid_attrs)
  end

  test "does not create resource and renders errors when data is invalid", %{conn: conn} do
    conn = post conn, door_path(conn, :create), door: @invalid_attrs
    assert html_response(conn, 200) =~ "New door"
  end

  test "shows chosen resource", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = get conn, door_path(conn, :show, door)
    assert html_response(conn, 200) =~ "<h1>#{door.name}</h1>"
  end

  test "renders page not found when id is nonexistent", %{conn: conn} do
    assert_raise Ecto.NoResultsError, fn ->
      get conn, door_path(conn, :show, -1)
    end
  end

  test "renders form for editing chosen resource", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = get conn, door_path(conn, :edit, door)
    assert html_response(conn, 200) =~ "Edit door"
  end

  test "updates chosen resource and redirects when data is valid", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = put conn, door_path(conn, :update, door), door: @valid_attrs
    assert redirected_to(conn) == door_path(conn, :show, door)
    assert Repo.get_by(Door, @valid_attrs)
  end

  test "updates door group with associated doors and redirects when data is valid", %{conn: conn} do
    door_group = create_door_group
    door = create_door
    # We can't dynamically construct a map with a variable without this
    # See: http://stackoverflow.com/questions/29837103/how-to-put-key-value-pair-into-map-with-variable-key-name
    # "Note that variables cannot be used as keys to add items to a map:"
    # See: http://elixir-lang.org/getting-started/maps-and-dicts.html#maps
    doors_param = Map.put(%{}, "#{door.id}", "on")
    conn = put conn, door_group_path(conn, :update, door_group), door_group: Dict.merge(@valid_attrs, %{doors: doors_param })
    assert redirected_to(conn) == door_group_path(conn, :show, door_group)
    door_group = Repo.get_by(DoorGroup, @valid_attrs) |> Repo.preload :doors
    assert door_group
    assert [door.id] == Enum.map(door_group.doors, &(&1.id))
  end

  test "removes unchecked associated door groups from a company", %{conn: conn} do
    door_group = create_door_group
    door = create_door
    Repo.insert! %Gatekeeper.DoorGroupDoor{door_id: door.id, door_group_id: door_group.id}

    conn = put conn, door_group_path(conn, :update, door_group), door_group: Dict.merge(@valid_attrs, id: door_group.id)
    assert redirected_to(conn) == door_group_path(conn, :show, door_group)
    door_group = Repo.get!(DoorGroup, door_group.id) |> Repo.preload :doors
    assert door_group
    assert [] == Enum.map(door_group.doors, &(&1.id))
  end

  test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = put conn, door_path(conn, :update, door), door: @invalid_attrs
    assert html_response(conn, 200) =~ "Edit door"
  end

  test "deletes chosen resource", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = delete conn, door_path(conn, :delete, door)
    assert redirected_to(conn) == door_path(conn, :index)
    refute Repo.get(Door, door.id)
  end
end

defmodule Gatekeeper.DoorControllerTest do
  use Gatekeeper.ConnCase

  import Gatekeeper.Factory

  alias Gatekeeper.Door
  alias Gatekeeper.DoorGroup

  @valid_attrs %{name: "some content"}
  @invalid_attrs %{}

  setup do
    conn = conn()
    {:ok, conn: conn}
  end

  test "lists all entries on index", %{conn: conn} do
    conn = get conn, door_path(conn, :index)
    assert html_response(conn, 200) =~ "Listing doors"
  end

  test "renders form for new resources", %{conn: conn} do
    conn = get conn, door_path(conn, :new)
    assert html_response(conn, 200) =~ "New door"
  end

  test "creates resource and redirects when data is valid", %{conn: conn} do
    conn = post conn, door_path(conn, :create), door: @valid_attrs
    assert redirected_to(conn) == door_path(conn, :index)
    assert Repo.get_by(Door, @valid_attrs)
  end

  test "does not create resource and renders errors when data is invalid", %{conn: conn} do
    conn = post conn, door_path(conn, :create), door: @invalid_attrs
    assert html_response(conn, 200) =~ "New door"
  end

  test "shows chosen resource", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = get conn, door_path(conn, :show, door)
    assert html_response(conn, 200) =~ "Show door"
  end

  test "renders page not found when id is nonexistent", %{conn: conn} do
    assert_raise Ecto.NoResultsError, fn ->
      get conn, door_path(conn, :show, -1)
    end
  end

  test "renders form for editing chosen resource", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = get conn, door_path(conn, :edit, door)
    assert html_response(conn, 200) =~ "Edit door"
  end

  test "updates chosen resource and redirects when data is valid", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = put conn, door_path(conn, :update, door), door: @valid_attrs
    assert redirected_to(conn) == door_path(conn, :show, door)
    assert Repo.get_by(Door, @valid_attrs)
  end

  test "updates door group with associated doors and redirects when data is valid", %{conn: conn} do
    door_group = create_door_group
    door = create_door
    # We can't dynamically construct a map with a variable without this
    # See: http://stackoverflow.com/questions/29837103/how-to-put-key-value-pair-into-map-with-variable-key-name
    # "Note that variables cannot be used as keys to add items to a map:"
    # See: http://elixir-lang.org/getting-started/maps-and-dicts.html#maps
    doors_param = Map.put(%{}, "#{door.id}", "on")
    conn = put conn, door_group_path(conn, :update, door_group), door_group: Dict.merge(@valid_attrs, %{doors: doors_param })
    assert redirected_to(conn) == door_group_path(conn, :show, door_group)
    door_group = Repo.get_by(DoorGroup, @valid_attrs) |> Repo.preload :doors
    assert door_group
    assert [door.id] == Enum.map(door_group.doors, &(&1.id))
  end

  test "removes unchecked associated door groups from a company", %{conn: conn} do
    door_group = create_door_group
    door = create_door
    Repo.insert! %Gatekeeper.DoorGroupDoor{door_id: door.id, door_group_id: door_group.id}

    conn = put conn, door_group_path(conn, :update, door_group), door_group: Dict.merge(@valid_attrs, id: door_group.id)
    assert redirected_to(conn) == door_group_path(conn, :show, door_group)
    door_group = Repo.get!(DoorGroup, door_group.id) |> Repo.preload :doors
    assert door_group
    assert [] == Enum.map(door_group.doors, &(&1.id))
  end

  test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = put conn, door_path(conn, :update, door), door: @invalid_attrs
    assert html_response(conn, 200) =~ "Edit door"
  end

  test "deletes chosen resource", %{conn: conn} do
    door = Repo.insert! %Door{}
    conn = delete conn, door_path(conn, :delete, door)
    assert redirected_to(conn) == door_path(conn, :index)
    refute Repo.get(Door, door.id)
  end
end


Use a better tag for integration tests

Use a better tag for integration tests

defmodule IntegrationTest do
  use ExUnit.Case, async: false

  alias Mix.Releases.Utils
  import MixTestHelper

  @moduletag integration: true
  @moduletag timeout: 60_000 * 5

  @standard_app_path Path.join([__DIR__, "fixtures", "standard_app"])
  @standard_output_path Path.join([
                          __DIR__,
                          "fixtures",
                          "standard_app",
                          "_build",
                          "prod",
                          "rel",
                          "standard_app"
                        ])

  defmacrop with_standard_app(body) do
    quote do
      clean_up_standard_app!()
      old_dir = File.cwd!()
      File.cd!(@standard_app_path)
      {:ok, _} = File.rm_rf(Path.join(@standard_app_path, "_build"))
      _ = File.rm(Path.join(@standard_app_path, "mix.lock"))
      {:ok, _} = mix("deps.get")
      {:ok, _} = mix("compile")
      {:ok, _} = mix("release.clean")
      unquote(body)
      File.cd!(old_dir)
    end
  end

  defp run_cmd(command, args) when is_list(args) do
    case System.cmd(command, args) do
      {output, 0} ->
        if System.get_env("VERBOSE_TESTS") do
          IO.puts(output)
        end

        {:ok, output}

      {output, non_zero_exit} ->
        IO.puts(output)
        {:error, non_zero_exit, output}
    end
  end

  {:ok, req} = Version.parse_requirement("< 1.4.0")

  if Version.match?(Version.parse!(System.version()), req) do
    @time_unit :milliseconds
  else
    @time_unit :millisecond
  end

  # Wait for VM and application to start
  defp wait_for_app(bin_path) do
    parent = self()
    pid = spawn_link(fn -> ping_loop(bin_path, parent) end)
    do_wait_for_app(pid, 30_000)
  end

  defp do_wait_for_app(pid, time_remaining) when time_remaining <= 0 do
    send(pid, :die)
    :timeout
  end

  defp do_wait_for_app(pid, time_remaining) do
    start = System.monotonic_time(@time_unit)

    if System.get_env("VERBOSE_TESTS") do
      IO.puts("Waiting #{time_remaining}ms for app..")
    end

    receive do
      {:ok, :pong} ->
        :ok

      _other ->
        ts = System.monotonic_time(@time_unit)
        do_wait_for_app(pid, time_remaining - (ts - start))
    after
      time_remaining ->
        send(pid, :die)
        :timeout
    end
  end

  defp ping_loop(bin_path, parent) do
    case System.cmd(bin_path, ["ping"]) do
      {"pong\n", 0} ->
        send(parent, {:ok, :pong})

      {output, _exit_code} ->
        receive do
          :die ->
            if System.get_env("VERBOSE_TESTS") do
              IO.puts(output)
            end

            :ok
        after
          1_000 ->
            ping_loop(bin_path, parent)
        end
    end
  end

  describe "standard application" do
    test "can build release and start it" do
      with_standard_app do
        # Build release
        assert {:ok, output} = mix("release", ["--verbose", "--env=prod"])

        for callback <- ~w(before_assembly after_assembly before_package after_package) do
          assert output =~ "Prod Plugin - #{callback}"
        end

        refute String.contains?(output, "Release Plugin")

        assert ["0.0.1"] == Utils.get_release_versions(@standard_output_path)
        # Boot it, ping it, and shut it down
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          tarfile = Path.join([@standard_output_path, "releases", "0.0.1", "standard_app.tar.gz"])
          assert :ok = :erl_tar.extract('#{tarfile}', [{:cwd, '#{tmpdir}'}, :compressed])
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)

          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)

          assert {:ok, "2\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          # Additional config items should exist
          assert {:ok, ":bar\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :foo)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          File.rm_rf!(tmpdir)
          :ok
        end
      end
    end

    test "can build and deploy hot upgrade" do
      with_standard_app do
        # Build v1 release
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod"])
        update_config_and_code_to_v2()
        # Build v2 release
        assert {:ok, _} = mix("compile")
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod", "--upgrade"])
        assert ["0.0.2", "0.0.1"] == Utils.get_release_versions(@standard_output_path)
        # Deploy it
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          unpack_old_release(tmpdir)
          copy_new_release(tmpdir)

          # Boot it, ping it, upgrade it, rpc to verify, then shut it down
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)
          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(2)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(2)"])
          assert {:ok, output} = run_cmd(bin_path, ["upgrade", "0.0.2"])
          assert output =~ "Made release standard_app:0.0.2 permanent"
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.pop()"])
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.pop()"])

          assert {:ok, "4\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
              :ok
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          _ = File.rm_rf(tmpdir)
          clean_up_standard_app!()
          :ok
        end
      end
    end

    test "can build and deploy hot upgrade with previously generated appup" do
      with_standard_app do
        # Build v1 release
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod"])
        update_config_and_code_to_v2()
        # Build v2 release
        assert {:ok, _} = mix("compile")
        assert {:ok, _} = mix("release.gen.appup", ["--app=standard_app"])
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod", "--upgrade"])
        assert ["0.0.2", "0.0.1"] == Utils.get_release_versions(@standard_output_path)
        # Deploy it
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          unpack_old_release(tmpdir)
          copy_new_release(tmpdir)

          # Boot it, ping it, upgrade it, rpc to verify, then shut it down
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)
          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(2)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(2)"])
          assert {:ok, output} = run_cmd(bin_path, ["upgrade", "0.0.2"])
          assert output =~ "Made release standard_app:0.0.2 permanent"
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.pop()"])
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.pop()"])

          assert {:ok, "4\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
              :ok
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          _ = File.rm_rf(tmpdir)
          clean_up_standard_app!()
          :ok
        end
      end
    end

    test "when installation directory contains a space" do
      with_standard_app do
        # Build v1 release
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod"])

        # Untar the release into a path that contains a space character then
        # try to run it.
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        tmpdir = Path.join(tmpdir, "dir with space")
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          tarfile = Path.join([@standard_output_path, "releases", "0.0.1", "standard_app.tar.gz"])
          assert :ok = :erl_tar.extract('#{tarfile}', [{:cwd, '#{tmpdir}'}, :compressed])
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)

          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)

          assert {:ok, "2\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          # Additional config items should exist
          assert {:ok, ":bar\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :foo)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          File.rm_rf!(tmpdir)
          :ok
        end
      end
    end
  end

  # Create a configuration file inside the release directory
  defp create_additional_config_file(directory) do
    extra_config_path = Path.join([directory, "extra.config"])
    Mix.Releases.Utils.write_term(extra_config_path, standard_app: [foo: :bar])
    :ok
  end

  defp clean_up_standard_app! do
    project_path = Path.join(@standard_app_path, "mix.exs.v1")

    if File.exists?(project_path) do
      File.cp!(project_path, Path.join(@standard_app_path, "mix.exs"))
      File.rm!(project_path)
    end

    config_path = Path.join([@standard_app_path, "config", "config.exs.v1"])

    if File.exists?(config_path) do
      File.cp!(config_path, Path.join([@standard_app_path, "config", "config.exs"]))
      File.rm!(config_path)
    end

    rel_config_path = Path.join([@standard_app_path, "rel", "config.exs.v1"])

    if File.exists?(rel_config_path) do
      File.cp!(rel_config_path, Path.join([@standard_app_path, "rel", "config.exs"]))
      File.rm!(rel_config_path)
    end

    a_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "a.ex.v1"])

    if File.exists?(a_mod_path) do
      File.cp!(a_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "a.ex"]))
      File.rm!(a_mod_path)
    end

    b_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "b.ex.v1"])

    if File.exists?(b_mod_path) do
      File.cp!(b_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "b.ex"]))
      File.rm!(b_mod_path)
    end

    File.rm_rf!(Path.join([@standard_app_path, "rel", "standard_app"]))
    File.rm_rf!(Path.join([@standard_app_path, "rel", "appups", "standard_app"]))
    :ok
  end

  defp update_config_and_code_to_v2 do
    # Update config for v2
    project_config_path = Path.join(@standard_app_path, "mix.exs")
    project = File.read!(project_config_path)
    config_path = Path.join([@standard_app_path, "config", "config.exs"])
    config = File.read!(config_path)
    rel_config_path = Path.join([@standard_app_path, "rel", "config.exs"])
    rel_config = File.read!(rel_config_path)
    # Write updates to modules
    a_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "a.ex"])
    a_mod = File.read!(a_mod_path)
    b_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "b.ex"])
    b_mod = File.read!(b_mod_path)
    # Save orig
    File.cp!(project_config_path, Path.join(@standard_app_path, "mix.exs.v1"))
    File.cp!(config_path, Path.join([@standard_app_path, "config", "config.exs.v1"]))
    File.cp!(rel_config_path, Path.join([@standard_app_path, "rel", "config.exs.v1"]))
    File.cp!(a_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "a.ex.v1"]))
    File.cp!(b_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "b.ex.v1"]))
    # Write new config
    new_project_config = String.replace(project, "version: \"0.0.1\"", "version: \"0.0.2\"")
    new_config = String.replace(config, "num_procs: 2", "num_procs: 4")

    new_rel_config =
    String.replace(rel_config, "set version: \"0.0.1\"", "set version: \"0.0.2\"")

    File.write!(project_config_path, new_project_config)
    File.write!(config_path, new_config)
    File.write!(rel_config_path, new_rel_config)
    # Write updated modules
    new_a_mod = String.replace(a_mod, "{:ok, {1, []}}", "{:ok, {2, []}}")

    new_b_mod =
    String.replace(b_mod, "loop({1, []}, parent, debug)", "loop({2, []}, parent, debug)")

    File.write!(a_mod_path, new_a_mod)
    File.write!(b_mod_path, new_b_mod)
  end

  defp unpack_old_release(tmpdir) do
    tarfile = Path.join([@standard_output_path, "releases", "0.0.1", "standard_app.tar.gz"])
    assert :ok = :erl_tar.extract('#{tarfile}', [{:cwd, '#{tmpdir}'}, :compressed])
  end

  defp copy_new_release(tmpdir) do
    File.mkdir_p!(Path.join([tmpdir, "releases", "0.0.2"]))
    File.cp!(
      Path.join([@standard_output_path, "releases", "0.0.2", "standard_app.tar.gz"]),
      Path.join([tmpdir, "releases", "0.0.2", "standard_app.tar.gz"])
    )
  end
end

defmodule IntegrationTest do
  use ExUnit.Case, async: false

  alias Mix.Releases.Utils
  import MixTestHelper

  @standard_app_path Path.join([__DIR__, "fixtures", "standard_app"])
  @standard_output_path Path.join([
                          __DIR__,
                          "fixtures",
                          "standard_app",
                          "_build",
                          "prod",
                          "rel",
                          "standard_app"
                        ])

  defmacrop with_standard_app(body) do
    quote do
      clean_up_standard_app!()
      old_dir = File.cwd!()
      File.cd!(@standard_app_path)
      {:ok, _} = File.rm_rf(Path.join(@standard_app_path, "_build"))
      _ = File.rm(Path.join(@standard_app_path, "mix.lock"))
      {:ok, _} = mix("deps.get")
      {:ok, _} = mix("compile")
      {:ok, _} = mix("release.clean")
      unquote(body)
      File.cd!(old_dir)
    end
  end

  defp run_cmd(command, args) when is_list(args) do
    case System.cmd(command, args) do
      {output, 0} ->
        if System.get_env("VERBOSE_TESTS") do
          IO.puts(output)
        end

        {:ok, output}

      {output, non_zero_exit} ->
        IO.puts(output)
        {:error, non_zero_exit, output}
    end
  end

  {:ok, req} = Version.parse_requirement("< 1.4.0")

  if Version.match?(Version.parse!(System.version()), req) do
    @time_unit :milliseconds
  else
    @time_unit :millisecond
  end

  # Wait for VM and application to start
  defp wait_for_app(bin_path) do
    parent = self()
    pid = spawn_link(fn -> ping_loop(bin_path, parent) end)
    do_wait_for_app(pid, 30_000)
  end

  defp do_wait_for_app(pid, time_remaining) when time_remaining <= 0 do
    send(pid, :die)
    :timeout
  end

  defp do_wait_for_app(pid, time_remaining) do
    start = System.monotonic_time(@time_unit)

    if System.get_env("VERBOSE_TESTS") do
      IO.puts("Waiting #{time_remaining}ms for app..")
    end

    receive do
      {:ok, :pong} ->
        :ok

      _other ->
        ts = System.monotonic_time(@time_unit)
        do_wait_for_app(pid, time_remaining - (ts - start))
    after
      time_remaining ->
        send(pid, :die)
        :timeout
    end
  end

  defp ping_loop(bin_path, parent) do
    case System.cmd(bin_path, ["ping"]) do
      {"pong\n", 0} ->
        send(parent, {:ok, :pong})

      {output, _exit_code} ->
        receive do
          :die ->
            if System.get_env("VERBOSE_TESTS") do
              IO.puts(output)
            end

            :ok
        after
          1_000 ->
            ping_loop(bin_path, parent)
        end
    end
  end

  describe "standard application" do
    @tag :expensive
    # 5m
    @tag timeout: 60_000 * 5
    test "can build release and start it" do
      with_standard_app do
        # Build release
        assert {:ok, output} = mix("release", ["--verbose", "--env=prod"])

        for callback <- ~w(before_assembly after_assembly before_package after_package) do
          assert output =~ "Prod Plugin - #{callback}"
        end

        refute String.contains?(output, "Release Plugin")

        assert ["0.0.1"] == Utils.get_release_versions(@standard_output_path)
        # Boot it, ping it, and shut it down
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          tarfile = Path.join([@standard_output_path, "releases", "0.0.1", "standard_app.tar.gz"])
          assert :ok = :erl_tar.extract('#{tarfile}', [{:cwd, '#{tmpdir}'}, :compressed])
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)

          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)

          assert {:ok, "2\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          # Additional config items should exist
          assert {:ok, ":bar\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :foo)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          File.rm_rf!(tmpdir)
          :ok
        end
      end
    end

    @tag :expensive
    # 5m
    @tag timeout: 60_000 * 5
    test "can build and deploy hot upgrade" do
      with_standard_app do
        # Build v1 release
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod"])
        update_config_and_code_to_v2()
        # Build v2 release
        assert {:ok, _} = mix("compile")
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod", "--upgrade"])
        assert ["0.0.2", "0.0.1"] == Utils.get_release_versions(@standard_output_path)
        # Deploy it
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          unpack_old_release(tmpdir)
          copy_new_release(tmpdir)

          # Boot it, ping it, upgrade it, rpc to verify, then shut it down
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)
          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(2)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(2)"])
          assert {:ok, output} = run_cmd(bin_path, ["upgrade", "0.0.2"])
          assert output =~ "Made release standard_app:0.0.2 permanent"
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.pop()"])
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.pop()"])

          assert {:ok, "4\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
              :ok
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          _ = File.rm_rf(tmpdir)
          clean_up_standard_app!()
          :ok
        end
      end
    end

    @tag :expensive
    # 5m
    @tag timeout: 60_000 * 5
    test "can build and deploy hot upgrade with previously generated appup" do
      with_standard_app do
        # Build v1 release
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod"])
        update_config_and_code_to_v2()
        # Build v2 release
        assert {:ok, _} = mix("compile")
        assert {:ok, _} = mix("release.gen.appup", ["--app=standard_app"])
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod", "--upgrade"])
        assert ["0.0.2", "0.0.1"] == Utils.get_release_versions(@standard_output_path)
        # Deploy it
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          unpack_old_release(tmpdir)
          copy_new_release(tmpdir)

          # Boot it, ping it, upgrade it, rpc to verify, then shut it down
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)
          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.push(2)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(1)"])
          assert {:ok, ":ok\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.push(2)"])
          assert {:ok, output} = run_cmd(bin_path, ["upgrade", "0.0.2"])
          assert output =~ "Made release standard_app:0.0.2 permanent"
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.A.pop()"])
          assert {:ok, "{:ok, 2}\n"} = run_cmd(bin_path, ["rpc", "StandardApp.B.pop()"])

          assert {:ok, "4\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
              :ok
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          _ = File.rm_rf(tmpdir)
          clean_up_standard_app!()
          :ok
        end
      end
    end

    @tag :expensive
    # 5m
    @tag timeout: 60_000 * 5
    test "when installation directory contains a space" do
      with_standard_app do
        # Build v1 release
        assert {:ok, _} = mix("release", ["--verbose", "--env=prod"])

        # Untar the release into a path that contains a space character then
        # try to run it.
        assert {:ok, tmpdir} = Utils.insecure_mkdir_temp()
        tmpdir = Path.join(tmpdir, "dir with space")
        bin_path = Path.join([tmpdir, "bin", "standard_app"])

        try do
          tarfile = Path.join([@standard_output_path, "releases", "0.0.1", "standard_app.tar.gz"])
          assert :ok = :erl_tar.extract('#{tarfile}', [{:cwd, '#{tmpdir}'}, :compressed])
          assert File.exists?(bin_path)

          case :os.type() do
            {:win32, _} ->
              assert {:ok, _} = run_cmd(bin_path, ["install"])

            _ ->
              :ok
          end

          :ok = create_additional_config_file(tmpdir)

          assert {:ok, _} = run_cmd(bin_path, ["start"])
          assert :ok = wait_for_app(bin_path)

          assert {:ok, "2\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :num_procs)"])

          # Additional config items should exist
          assert {:ok, ":bar\n"} =
                   run_cmd(bin_path, ["rpc", "Application.get_env(:standard_app, :foo)"])

          case :os.type() do
            {:win32, _} ->
              assert {:ok, output} = run_cmd(bin_path, ["stop"])
              assert output =~ "stopped"
              assert {:ok, _} = run_cmd(bin_path, ["uninstall"])

            _ ->
              assert {:ok, "ok\n"} = run_cmd(bin_path, ["stop"])
          end
        rescue
          e ->
            run_cmd(bin_path, ["stop"])

            case :os.type() do
              {:win32, _} ->
                run_cmd(bin_path, ["uninstall"])

              _ ->
                :ok
            end

            reraise e, System.stacktrace()
        after
          File.rm_rf!(tmpdir)
          :ok
        end
      end
    end
  end

  # Create a configuration file inside the release directory
  defp create_additional_config_file(directory) do
    extra_config_path = Path.join([directory, "extra.config"])
    Mix.Releases.Utils.write_term(extra_config_path, standard_app: [foo: :bar])
    :ok
  end

  defp clean_up_standard_app! do
    project_path = Path.join(@standard_app_path, "mix.exs.v1")

    if File.exists?(project_path) do
      File.cp!(project_path, Path.join(@standard_app_path, "mix.exs"))
      File.rm!(project_path)
    end

    config_path = Path.join([@standard_app_path, "config", "config.exs.v1"])

    if File.exists?(config_path) do
      File.cp!(config_path, Path.join([@standard_app_path, "config", "config.exs"]))
      File.rm!(config_path)
    end

    rel_config_path = Path.join([@standard_app_path, "rel", "config.exs.v1"])

    if File.exists?(rel_config_path) do
      File.cp!(rel_config_path, Path.join([@standard_app_path, "rel", "config.exs"]))
      File.rm!(rel_config_path)
    end

    a_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "a.ex.v1"])

    if File.exists?(a_mod_path) do
      File.cp!(a_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "a.ex"]))
      File.rm!(a_mod_path)
    end

    b_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "b.ex.v1"])

    if File.exists?(b_mod_path) do
      File.cp!(b_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "b.ex"]))
      File.rm!(b_mod_path)
    end

    File.rm_rf!(Path.join([@standard_app_path, "rel", "standard_app"]))
    File.rm_rf!(Path.join([@standard_app_path, "rel", "appups", "standard_app"]))
    :ok
  end

  defp update_config_and_code_to_v2 do
    # Update config for v2
    project_config_path = Path.join(@standard_app_path, "mix.exs")
    project = File.read!(project_config_path)
    config_path = Path.join([@standard_app_path, "config", "config.exs"])
    config = File.read!(config_path)
    rel_config_path = Path.join([@standard_app_path, "rel", "config.exs"])
    rel_config = File.read!(rel_config_path)
    # Write updates to modules
    a_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "a.ex"])
    a_mod = File.read!(a_mod_path)
    b_mod_path = Path.join([@standard_app_path, "lib", "standard_app", "b.ex"])
    b_mod = File.read!(b_mod_path)
    # Save orig
    File.cp!(project_config_path, Path.join(@standard_app_path, "mix.exs.v1"))
    File.cp!(config_path, Path.join([@standard_app_path, "config", "config.exs.v1"]))
    File.cp!(rel_config_path, Path.join([@standard_app_path, "rel", "config.exs.v1"]))
    File.cp!(a_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "a.ex.v1"]))
    File.cp!(b_mod_path, Path.join([@standard_app_path, "lib", "standard_app", "b.ex.v1"]))
    # Write new config
    new_project_config = String.replace(project, "version: \"0.0.1\"", "version: \"0.0.2\"")
    new_config = String.replace(config, "num_procs: 2", "num_procs: 4")

    new_rel_config =
    String.replace(rel_config, "set version: \"0.0.1\"", "set version: \"0.0.2\"")

    File.write!(project_config_path, new_project_config)
    File.write!(config_path, new_config)
    File.write!(rel_config_path, new_rel_config)
    # Write updated modules
    new_a_mod = String.replace(a_mod, "{:ok, {1, []}}", "{:ok, {2, []}}")

    new_b_mod =
    String.replace(b_mod, "loop({1, []}, parent, debug)", "loop({2, []}, parent, debug)")

    File.write!(a_mod_path, new_a_mod)
    File.write!(b_mod_path, new_b_mod)
  end

  defp unpack_old_release(tmpdir) do
    tarfile = Path.join([@standard_output_path, "releases", "0.0.1", "standard_app.tar.gz"])
    assert :ok = :erl_tar.extract('#{tarfile}', [{:cwd, '#{tmpdir}'}, :compressed])
  end

  defp copy_new_release(tmpdir) do
    File.mkdir_p!(Path.join([tmpdir, "releases", "0.0.2"]))
    File.cp!(
      Path.join([@standard_output_path, "releases", "0.0.2", "standard_app.tar.gz"]),
      Path.join([tmpdir, "releases", "0.0.2", "standard_app.tar.gz"])
    )
  end
end


Added ability to replace default layout. #77

defmodule ExAdmin.Web do
  @moduledoc false

  def model do
    quote do
      use Ecto.Schema

      import Ecto
      import Ecto.Changeset
      import Ecto.Query, only: [from: 1, from: 2]
    end
  end

  def controller do
    quote do
      use Phoenix.Controller

      import Ecto.Model
      import Ecto.Query, only: [from: 1, from: 2]

      import ExAdmin.Router.Helpers
      import ExAdmin.Utils, only: [admin_path: 0, admin_path: 2, admin_resource_path: 3, admin_association_path: 4]
      import ExAdmin.Controller

      defp set_theme(conn, _) do
        assign(conn, :theme, ExAdmin.theme)
      end

      defp set_layout(conn, _) do
        layout = Application.get_env(:ex_admin, :layout) || "#{conn.assigns.theme.name}.html"
        put_layout(conn, layout)
      end
    end
  end

  def view do
    quote do
      require Logger

      file_path = __ENV__.file
      |> Path.dirname
      |> String.split("/views")
      |> hd
      |> Path.join("templates")

      use Phoenix.View, root: file_path
      # use Phoenix.View, root: "web/templates"

      # Import convenience functions from controllers
      import Phoenix.Controller, only: [view_module: 1]

      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      import ExAdmin.Router.Helpers
      #import ExAuth
      import ExAdmin.ViewHelpers
    end
  end

  def router do
    quote do
      use Phoenix.Router
    end
  end

  def channel do
    quote do
      use Phoenix.Channel

      # alias Application.get_env(:ex_admin, :repo)
      # import Application.get_env(:ex_admin, :repo)
      import Ecto
      import Ecto.Query, only: [from: 1, from: 2]

    end
  end

  @doc """
  When used, dispatch to the appropriate controller/view/etc.
  """
  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
end

defmodule ExAdmin.Web do
  @moduledoc false

  def model do
    quote do
      use Ecto.Schema

      import Ecto
      import Ecto.Changeset
      import Ecto.Query, only: [from: 1, from: 2]
    end
  end

  def controller do
    quote do
      use Phoenix.Controller

      import Ecto.Model
      import Ecto.Query, only: [from: 1, from: 2]

      import ExAdmin.Router.Helpers
      import ExAdmin.Utils, only: [admin_path: 0, admin_path: 2, admin_resource_path: 3, admin_association_path: 4]
      import ExAdmin.Controller

      defp set_theme(conn, _) do
        assign(conn, :theme, ExAdmin.theme)
      end

      defp set_layout(conn, _) do
        put_layout(conn, "#{conn.assigns.theme.name}.html")
      end
    end
  end

  def view do
    quote do
      require Logger

      file_path = __ENV__.file
      |> Path.dirname
      |> String.split("/views")
      |> hd
      |> Path.join("templates")

      use Phoenix.View, root: file_path
      # use Phoenix.View, root: "web/templates"

      # Import convenience functions from controllers
      import Phoenix.Controller, only: [view_module: 1]

      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      import ExAdmin.Router.Helpers
      #import ExAuth
      import ExAdmin.ViewHelpers
    end
  end

  def router do
    quote do
      use Phoenix.Router
    end
  end

  def channel do
    quote do
      use Phoenix.Channel

      # alias Application.get_env(:ex_admin, :repo)
      # import Application.get_env(:ex_admin, :repo)
      import Ecto
      import Ecto.Query, only: [from: 1, from: 2]

    end
  end

  @doc """
  When used, dispatch to the appropriate controller/view/etc.
  """
  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
end


Add "skipped" action result

Add "skipped" action result

Fixes #1109

require Logger

defmodule BorsNG.GitHub do
  @moduledoc """
  Wrappers around the GitHub REST API.
  """

  @typedoc """
  An authentication token;
  this may be a raw token (as on oAuth)
  or an installation xref (in which case the server will look it up).
  """
  @type ttoken :: {:installation, number} | {:raw, binary}

  @typedoc """
  A repository connection;
  it packages a repository with the permissions to access it.
  """
  @type tconn :: {ttoken, number} | {ttoken, number}

  @type tuser :: BorsNG.GitHub.User.t()
  @type trepo :: BorsNG.GitHub.Repo.t()
  @type tpr :: BorsNG.GitHub.Pr.t()
  @type tstatus :: :ok | :running | :error
  @type trepo_perm :: :admin | :push | :pull
  @type tuser_repo_perms :: %{admin: boolean, push: boolean, pull: boolean}
  @type tcollaborator :: %{user: tuser, perms: tuser_repo_perms}
  @type tcommitter :: %{name: bitstring, email: bitstring}

  @spec get_pr_files!(tconn, integer) :: [BorsNG.GitHub.File.t()]
  def get_pr_files!(repo_conn, pr_xref) do
    {:ok, pr} = get_pr_files(repo_conn, pr_xref)
    pr
  end

  @spec get_pr_files(tconn, integer) ::
          {:ok, [BorsNG.GitHub.File.t()]} | {:error, term}
  def get_pr_files(repo_conn, pr_xref) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_pr_files, repo_conn, {pr_xref}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_pr!(tconn, integer | bitstring) :: BorsNG.GitHub.Pr.t()
  def get_pr!(repo_conn, pr_xref) do
    {:ok, pr} = get_pr(repo_conn, pr_xref)
    pr
  end

  @spec get_pr(tconn, integer | bitstring) ::
          {:ok, BorsNG.GitHub.Pr.t()} | {:error, term}
  def get_pr(repo_conn, pr_xref) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_pr, repo_conn, {pr_xref}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec update_pr_base!(tconn, BorsNG.GitHub.Pr.t()) :: BorsNG.GitHub.Pr.t()
  def update_pr_base!(repo_conn, pr) do
    {:ok, pr} = update_pr_base(repo_conn, pr)
    pr
  end

  @spec update_pr!(tconn, BorsNG.GitHub.Pr.t()) :: BorsNG.GitHub.Pr.t()
  def update_pr!(repo_conn, pr) do
    {:ok, pr} = update_pr(repo_conn, pr)
    pr
  end

  @spec update_pr_base(tconn, BorsNG.GitHub.Pr.t()) ::
          {:ok, BorsNG.GitHub.Pr.t()} | {:error, term}
  def update_pr_base(repo_conn, pr) do
    GenServer.call(
      BorsNG.GitHub,
      {:update_pr_base, repo_conn, pr},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec update_pr(tconn, BorsNG.GitHub.Pr.t()) ::
          {:ok, BorsNG.GitHub.Pr.t()} | {:error, term}
  def update_pr(repo_conn, pr) do
    GenServer.call(
      BorsNG.GitHub,
      {:update_pr, repo_conn, pr},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_pr_commits!(tconn, integer | bitstring) :: [BorsNG.GitHub.Commit.t()]
  def get_pr_commits!(repo_conn, pr_xref) do
    {:ok, commits} = get_pr_commits(repo_conn, pr_xref)
    commits
  end

  @spec get_pr_commits(tconn, integer | bitstring) ::
          {:ok, [BorsNG.GitHub.Commit.t()]} | {:error, term}
  def get_pr_commits(repo_conn, pr_xref) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_pr_commits, repo_conn, {pr_xref}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_open_prs!(tconn) :: [tpr]
  def get_open_prs!(repo_conn) do
    {:ok, prs} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_open_prs, repo_conn, {}},
        100_000
      )

    prs
  end

  @spec get_open_prs_with_base!(tconn, binary) :: [tpr]
  def get_open_prs_with_base!(repo_conn, base) do
    {:ok, prs} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_open_prs_with_base, repo_conn, {base}},
        100_000
      )

    prs
  end

  @spec push!(tconn, binary, binary) :: binary
  def push!(repo_conn, sha, to) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:push, repo_conn, {sha, to}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec push(tconn, binary, binary) :: {:ok, binary} | {:error, term, term, term}
  def push(repo_conn, sha, to) do
    GenServer.call(
      BorsNG.GitHub,
      {:push, repo_conn, {sha, to}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_branch!(tconn, binary) :: %{commit: bitstring, tree: bitstring}
  def get_branch!(repo_conn, from) do
    {:ok, commit} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_branch, repo_conn, {from}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    commit
  end

  @spec delete_branch!(tconn, binary) :: :ok
  def delete_branch!(repo_conn, branch) do
    :ok =
      GenServer.call(
        BorsNG.GitHub,
        {:delete_branch, repo_conn, {branch}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    :ok
  end

  @spec merge_branch!(tconn, %{
          from: bitstring,
          to: bitstring,
          commit_message: bitstring
        }) :: %{commit: binary, tree: binary} | :conflict
  def merge_branch!(repo_conn, info) do
    {:ok, commit} =
      GenServer.call(
        BorsNG.GitHub,
        {:merge_branch, repo_conn, {info}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    commit
  end

  @spec synthesize_commit!(tconn, %{
          branch: bitstring,
          tree: bitstring,
          parents: [bitstring],
          commit_message: bitstring,
          committer: tcommitter | nil
        }) :: binary
  def synthesize_commit!(repo_conn, info) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:synthesize_commit, repo_conn, {info}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec create_commit!(tconn, %{
          tree: bitstring,
          parents: [bitstring],
          commit_message: bitstring,
          committer: tcommitter | nil
        }) :: binary
  def create_commit!(repo_conn, info) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:create_commit, repo_conn, {info}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec create_commit(tconn, %{
          tree: bitstring,
          parents: [bitstring],
          commit_message: bitstring,
          committer: tcommitter | nil
        }) :: {:ok, binary} | {:error, term, term}
  def create_commit(repo_conn, info) do
    GenServer.call(
      BorsNG.GitHub,
      {:create_commit, repo_conn, {info}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec force_push!(tconn, binary, binary) :: binary
  def force_push!(repo_conn, sha, to) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:force_push, repo_conn, {sha, to}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec get_commit_status!(tconn, binary) :: %{
          binary => tstatus
        }
  def get_commit_status!(repo_conn, sha) do
    {:ok, status} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_commit_status, repo_conn, {sha}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    status
  end

  @spec get_labels!(tconn, integer | bitstring) :: [bitstring]
  def get_labels!(repo_conn, issue_xref) do
    {:ok, labels} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_labels, repo_conn, {issue_xref}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    labels
  end

  @spec get_reviews!(tconn, integer | bitstring) :: map
  def get_reviews!(repo_conn, issue_xref) do
    {:ok, labels} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_reviews, repo_conn, {issue_xref}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    labels
  end

  @spec get_commit_reviews!(tconn, integer | bitstring, binary) :: map
  def get_commit_reviews!(repo_conn, issue_xref, sha) do
    {:ok, labels} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_reviews, repo_conn, {issue_xref, sha}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    labels
  end

  @spec get_file!(tconn, binary, binary) :: binary | nil
  def get_file!(repo_conn, branch, path) do
    {:ok, file} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_file, repo_conn, {branch, path}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    file
  end

  @spec post_comment!(tconn, number, binary) :: :ok
  def post_comment!(repo_conn, number, body) do
    :ok =
      GenServer.call(
        BorsNG.GitHub,
        {:post_comment, repo_conn, {number, body}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    :ok
  end

  @spec post_commit_status!(tconn, {binary, tstatus, binary, binary}) :: :ok
  def post_commit_status!(repo_conn, {sha, status, msg, url}) do
    # Auto-retry
    first_try =
      GenServer.call(
        BorsNG.GitHub,
        {:post_commit_status, repo_conn, {sha, status, msg, url}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    case first_try do
      :ok ->
        :ok

      _ ->
        Process.sleep(11)

        :ok =
          GenServer.call(
            BorsNG.GitHub,
            {:post_commit_status, repo_conn, {sha, status, msg, url}},
            Confex.fetch_env!(:bors, :api_github_timeout)
          )
    end

    :ok
  end

  @spec get_user_by_login!(ttoken, binary) :: {:ok, tuser} | :error | nil
  def get_user_by_login!(token, login) do
    {:ok, user} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_user_by_login, token, {String.trim(login)}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    user
  end

  @spec belongs_to_team?(tconn, String.t(), String.t(), String.t()) ::
          boolean
  def belongs_to_team?(repo_conn, org, team_slug, username) do
    GenServer.call(
      BorsNG.GitHub,
      {:belongs_to_team, repo_conn, {org, team_slug, username}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_collaborators_by_repo(tconn) ::
          {:ok, [tcollaborator]} | :error
  def get_collaborators_by_repo(repo_conn) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_collaborators_by_repo, repo_conn, {}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_app!() :: String.t()
  def get_app! do
    {:ok, app_link} =
      GenServer.call(BorsNG.GitHub, :get_app, Confex.fetch_env!(:bors, :api_github_timeout))

    app_link
  end

  @spec get_installation_repos!(ttoken) :: [trepo]
  def get_installation_repos!(token) do
    {:ok, repos} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_installation_repos, token, {}},
        100_000
      )

    repos
  end

  @spec get_installation_list! :: [integer]
  def get_installation_list! do
    {:ok, installations} =
      GenServer.call(
        BorsNG.GitHub,
        :get_installation_list,
        100_000
      )

    installations
  end

  @spec map_state_to_status(binary) :: tstatus
  def map_state_to_status(state) do
    case state do
      "pending" -> :running
      "success" -> :ok
      "neutral" -> :ok
      "skipped" -> :ok
      "failure" -> :error
      "error" -> :error
    end
  end

  @spec map_check_to_status(binary) :: tstatus
  def map_check_to_status(conclusion) do
    case conclusion do
      nil -> :running
      "success" -> :ok
      _ -> :error
    end
  end

  @spec map_status_to_state(tstatus) :: binary
  def map_status_to_state(state) do
    case state do
      :running -> "pending"
      :ok -> "success"
      :error -> "failure"
    end
  end

  @spec map_changed_status(binary) :: binary
  def map_changed_status(check_name) do
    case check_name do
      "Travis CI - Branch" -> "continuous-integration/travis-ci/push"
      check_name -> check_name
    end
  end
end

require Logger

defmodule BorsNG.GitHub do
  @moduledoc """
  Wrappers around the GitHub REST API.
  """

  @typedoc """
  An authentication token;
  this may be a raw token (as on oAuth)
  or an installation xref (in which case the server will look it up).
  """
  @type ttoken :: {:installation, number} | {:raw, binary}

  @typedoc """
  A repository connection;
  it packages a repository with the permissions to access it.
  """
  @type tconn :: {ttoken, number} | {ttoken, number}

  @type tuser :: BorsNG.GitHub.User.t()
  @type trepo :: BorsNG.GitHub.Repo.t()
  @type tpr :: BorsNG.GitHub.Pr.t()
  @type tstatus :: :ok | :running | :error
  @type trepo_perm :: :admin | :push | :pull
  @type tuser_repo_perms :: %{admin: boolean, push: boolean, pull: boolean}
  @type tcollaborator :: %{user: tuser, perms: tuser_repo_perms}
  @type tcommitter :: %{name: bitstring, email: bitstring}

  @spec get_pr_files!(tconn, integer) :: [BorsNG.GitHub.File.t()]
  def get_pr_files!(repo_conn, pr_xref) do
    {:ok, pr} = get_pr_files(repo_conn, pr_xref)
    pr
  end

  @spec get_pr_files(tconn, integer) ::
          {:ok, [BorsNG.GitHub.File.t()]} | {:error, term}
  def get_pr_files(repo_conn, pr_xref) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_pr_files, repo_conn, {pr_xref}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_pr!(tconn, integer | bitstring) :: BorsNG.GitHub.Pr.t()
  def get_pr!(repo_conn, pr_xref) do
    {:ok, pr} = get_pr(repo_conn, pr_xref)
    pr
  end

  @spec get_pr(tconn, integer | bitstring) ::
          {:ok, BorsNG.GitHub.Pr.t()} | {:error, term}
  def get_pr(repo_conn, pr_xref) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_pr, repo_conn, {pr_xref}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec update_pr_base!(tconn, BorsNG.GitHub.Pr.t()) :: BorsNG.GitHub.Pr.t()
  def update_pr_base!(repo_conn, pr) do
    {:ok, pr} = update_pr_base(repo_conn, pr)
    pr
  end

  @spec update_pr!(tconn, BorsNG.GitHub.Pr.t()) :: BorsNG.GitHub.Pr.t()
  def update_pr!(repo_conn, pr) do
    {:ok, pr} = update_pr(repo_conn, pr)
    pr
  end

  @spec update_pr_base(tconn, BorsNG.GitHub.Pr.t()) ::
          {:ok, BorsNG.GitHub.Pr.t()} | {:error, term}
  def update_pr_base(repo_conn, pr) do
    GenServer.call(
      BorsNG.GitHub,
      {:update_pr_base, repo_conn, pr},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec update_pr(tconn, BorsNG.GitHub.Pr.t()) ::
          {:ok, BorsNG.GitHub.Pr.t()} | {:error, term}
  def update_pr(repo_conn, pr) do
    GenServer.call(
      BorsNG.GitHub,
      {:update_pr, repo_conn, pr},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_pr_commits!(tconn, integer | bitstring) :: [BorsNG.GitHub.Commit.t()]
  def get_pr_commits!(repo_conn, pr_xref) do
    {:ok, commits} = get_pr_commits(repo_conn, pr_xref)
    commits
  end

  @spec get_pr_commits(tconn, integer | bitstring) ::
          {:ok, [BorsNG.GitHub.Commit.t()]} | {:error, term}
  def get_pr_commits(repo_conn, pr_xref) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_pr_commits, repo_conn, {pr_xref}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_open_prs!(tconn) :: [tpr]
  def get_open_prs!(repo_conn) do
    {:ok, prs} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_open_prs, repo_conn, {}},
        100_000
      )

    prs
  end

  @spec get_open_prs_with_base!(tconn, binary) :: [tpr]
  def get_open_prs_with_base!(repo_conn, base) do
    {:ok, prs} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_open_prs_with_base, repo_conn, {base}},
        100_000
      )

    prs
  end

  @spec push!(tconn, binary, binary) :: binary
  def push!(repo_conn, sha, to) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:push, repo_conn, {sha, to}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec push(tconn, binary, binary) :: {:ok, binary} | {:error, term, term, term}
  def push(repo_conn, sha, to) do
    GenServer.call(
      BorsNG.GitHub,
      {:push, repo_conn, {sha, to}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_branch!(tconn, binary) :: %{commit: bitstring, tree: bitstring}
  def get_branch!(repo_conn, from) do
    {:ok, commit} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_branch, repo_conn, {from}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    commit
  end

  @spec delete_branch!(tconn, binary) :: :ok
  def delete_branch!(repo_conn, branch) do
    :ok =
      GenServer.call(
        BorsNG.GitHub,
        {:delete_branch, repo_conn, {branch}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    :ok
  end

  @spec merge_branch!(tconn, %{
          from: bitstring,
          to: bitstring,
          commit_message: bitstring
        }) :: %{commit: binary, tree: binary} | :conflict
  def merge_branch!(repo_conn, info) do
    {:ok, commit} =
      GenServer.call(
        BorsNG.GitHub,
        {:merge_branch, repo_conn, {info}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    commit
  end

  @spec synthesize_commit!(tconn, %{
          branch: bitstring,
          tree: bitstring,
          parents: [bitstring],
          commit_message: bitstring,
          committer: tcommitter | nil
        }) :: binary
  def synthesize_commit!(repo_conn, info) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:synthesize_commit, repo_conn, {info}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec create_commit!(tconn, %{
          tree: bitstring,
          parents: [bitstring],
          commit_message: bitstring,
          committer: tcommitter | nil
        }) :: binary
  def create_commit!(repo_conn, info) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:create_commit, repo_conn, {info}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec create_commit(tconn, %{
          tree: bitstring,
          parents: [bitstring],
          commit_message: bitstring,
          committer: tcommitter | nil
        }) :: {:ok, binary} | {:error, term, term}
  def create_commit(repo_conn, info) do
    GenServer.call(
      BorsNG.GitHub,
      {:create_commit, repo_conn, {info}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec force_push!(tconn, binary, binary) :: binary
  def force_push!(repo_conn, sha, to) do
    {:ok, sha} =
      GenServer.call(
        BorsNG.GitHub,
        {:force_push, repo_conn, {sha, to}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    sha
  end

  @spec get_commit_status!(tconn, binary) :: %{
          binary => tstatus
        }
  def get_commit_status!(repo_conn, sha) do
    {:ok, status} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_commit_status, repo_conn, {sha}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    status
  end

  @spec get_labels!(tconn, integer | bitstring) :: [bitstring]
  def get_labels!(repo_conn, issue_xref) do
    {:ok, labels} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_labels, repo_conn, {issue_xref}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    labels
  end

  @spec get_reviews!(tconn, integer | bitstring) :: map
  def get_reviews!(repo_conn, issue_xref) do
    {:ok, labels} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_reviews, repo_conn, {issue_xref}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    labels
  end

  @spec get_commit_reviews!(tconn, integer | bitstring, binary) :: map
  def get_commit_reviews!(repo_conn, issue_xref, sha) do
    {:ok, labels} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_reviews, repo_conn, {issue_xref, sha}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    labels
  end

  @spec get_file!(tconn, binary, binary) :: binary | nil
  def get_file!(repo_conn, branch, path) do
    {:ok, file} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_file, repo_conn, {branch, path}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    file
  end

  @spec post_comment!(tconn, number, binary) :: :ok
  def post_comment!(repo_conn, number, body) do
    :ok =
      GenServer.call(
        BorsNG.GitHub,
        {:post_comment, repo_conn, {number, body}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    :ok
  end

  @spec post_commit_status!(tconn, {binary, tstatus, binary, binary}) :: :ok
  def post_commit_status!(repo_conn, {sha, status, msg, url}) do
    # Auto-retry
    first_try =
      GenServer.call(
        BorsNG.GitHub,
        {:post_commit_status, repo_conn, {sha, status, msg, url}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    case first_try do
      :ok ->
        :ok

      _ ->
        Process.sleep(11)

        :ok =
          GenServer.call(
            BorsNG.GitHub,
            {:post_commit_status, repo_conn, {sha, status, msg, url}},
            Confex.fetch_env!(:bors, :api_github_timeout)
          )
    end

    :ok
  end

  @spec get_user_by_login!(ttoken, binary) :: {:ok, tuser} | :error | nil
  def get_user_by_login!(token, login) do
    {:ok, user} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_user_by_login, token, {String.trim(login)}},
        Confex.fetch_env!(:bors, :api_github_timeout)
      )

    user
  end

  @spec belongs_to_team?(tconn, String.t(), String.t(), String.t()) ::
          boolean
  def belongs_to_team?(repo_conn, org, team_slug, username) do
    GenServer.call(
      BorsNG.GitHub,
      {:belongs_to_team, repo_conn, {org, team_slug, username}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_collaborators_by_repo(tconn) ::
          {:ok, [tcollaborator]} | :error
  def get_collaborators_by_repo(repo_conn) do
    GenServer.call(
      BorsNG.GitHub,
      {:get_collaborators_by_repo, repo_conn, {}},
      Confex.fetch_env!(:bors, :api_github_timeout)
    )
  end

  @spec get_app!() :: String.t()
  def get_app! do
    {:ok, app_link} =
      GenServer.call(BorsNG.GitHub, :get_app, Confex.fetch_env!(:bors, :api_github_timeout))

    app_link
  end

  @spec get_installation_repos!(ttoken) :: [trepo]
  def get_installation_repos!(token) do
    {:ok, repos} =
      GenServer.call(
        BorsNG.GitHub,
        {:get_installation_repos, token, {}},
        100_000
      )

    repos
  end

  @spec get_installation_list! :: [integer]
  def get_installation_list! do
    {:ok, installations} =
      GenServer.call(
        BorsNG.GitHub,
        :get_installation_list,
        100_000
      )

    installations
  end

  @spec map_state_to_status(binary) :: tstatus
  def map_state_to_status(state) do
    case state do
      "pending" -> :running
      "success" -> :ok
      "neutral" -> :ok
      "failure" -> :error
      "error" -> :error
    end
  end

  @spec map_check_to_status(binary) :: tstatus
  def map_check_to_status(conclusion) do
    case conclusion do
      nil -> :running
      "success" -> :ok
      _ -> :error
    end
  end

  @spec map_status_to_state(tstatus) :: binary
  def map_status_to_state(state) do
    case state do
      :running -> "pending"
      :ok -> "success"
      :error -> "failure"
    end
  end

  @spec map_changed_status(binary) :: binary
  def map_changed_status(check_name) do
    case check_name do
      "Travis CI - Branch" -> "continuous-integration/travis-ci/push"
      check_name -> check_name
    end
  end
end