Add a bunch more test cases for command parsing

defmodule BorsNG.CommandTest do
  use ExUnit.Case

  alias BorsNG.Command
  alias BorsNG.Database.Installation
  alias BorsNG.Database.Project
  alias BorsNG.Database.Repo
  alias BorsNG.GitHub

  doctest BorsNG.Command

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)

    inst = %Installation{installation_xref: 91}
    |> Repo.insert!()
    proj = %Project{
      installation_id: inst.id,
      repo_xref: 14,
      staging_branch: "staging"}
    |> Repo.insert!()
    {:ok, inst: inst, proj: proj}
  end

  test "reject the empty string" do
    assert [] == Command.parse("")
    assert [] == Command.parse(nil)
  end

  test "reject strings without the phrase" do
    assert [] == Command.parse("doink!")
  end

  test "reject a string that merely starts out like a command" do
    assert [] == Command.parse("bors doink")
  end

  test "accept the bare command" do
    assert [{:try, ""}] == Command.parse("bors try")
    assert [:activate] == Command.parse("bors r+")
    assert [:deactivate] == Command.parse("bors r-")
  end

  test "accept priority" do
    assert [{:set_priority, 1}, :activate] == Command.parse("bors r+ p=1")
    assert [{:set_priority, 1}, {:activate_by, "me"}] ==
      Command.parse("bors r=me p=1")
    assert [{:set_priority, 1}] == Command.parse("bors p=1")
  end

  test "accept negative priority" do
    assert [{:set_priority, -1}, :activate] == Command.parse("bors r+ p=-1")
    assert [{:set_priority, -1}, {:activate_by, "me"}] ==
      Command.parse("bors r=me p=-1")
    assert [{:set_priority, -1}] == Command.parse("bors p=-1")
  end

  test "do not parse priority after try command" do
    assert [{:try, " p=1"}] == Command.parse("bors try p=1")
    assert [{:try, " p=screwy"}] == Command.parse("bors try p=screwy")
  end

  test "accept command with colon after it" do
    assert [{:try, ""}] == Command.parse("bors: try")
  end

  test "accept the try command with an argument" do
    assert [{:try, "-layout"}] == Command.parse("bors try-layout")
  end

  test "accept more than one command in a single comment" do
    expected = [
      {:try, ""},
      :deactivate]
    command = """
    bors try
    bors r-
    """
    assert expected == Command.parse(command)
  end

  test "accept the try command with more argumentation" do
    assert [{:try, " --layout --script"}] ==
      Command.parse("bors try --layout --script")
  end

  test "do not accept the command with a prefix" do
    assert [] == Command.parse("Xbors tryZ")
  end

  test "running ping command should post comment", %{proj: proj} do
    GitHub.ServerMock.put_state(%{
      {{:installation, 91}, 14} => %{
        branches: %{},
        comments: %{1 => []},
        statuses: %{}
      }
    })

    c = %Command{
      project: proj,
      commenter: "commenter",
      comment: "bors ping",
      pr_xref: 1
    }
    Command.run(c, :ping)
    assert GitHub.ServerMock.get_state() == %{
      {{:installation, 91}, 14} => %{
        branches: %{},
        comments: %{1 => ["pong"]},
        statuses: %{}
      }
    }
  end
end

defmodule BorsNG.CommandTest do
  use ExUnit.Case

  alias BorsNG.Command
  alias BorsNG.Database.Installation
  alias BorsNG.Database.Project
  alias BorsNG.Database.Repo
  alias BorsNG.GitHub

  doctest BorsNG.Command

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)

    inst = %Installation{installation_xref: 91}
    |> Repo.insert!()
    proj = %Project{
      installation_id: inst.id,
      repo_xref: 14,
      staging_branch: "staging"}
    |> Repo.insert!()
    {:ok, inst: inst, proj: proj}
  end

  test "reject the empty string" do
    assert [] == Command.parse("")
    assert [] == Command.parse(nil)
  end

  test "reject strings without the phrase" do
    assert [] == Command.parse("doink!")
  end

  test "reject a string that merely starts out like a command" do
    assert [] == Command.parse("bors doink")
  end

  test "accept the bare command" do
    assert [{:try, ""}] == Command.parse("bors try")
    assert [:activate] == Command.parse("bors r+")
    assert [:deactivate] == Command.parse("bors r-")
  end

  test "accept command with colon after it" do
    assert [{:try, ""}] == Command.parse("bors: try")
  end

  test "accept the try command with an argument" do
    assert [{:try, "-layout"}] == Command.parse("bors try-layout")
  end

  test "accept more than one command in a single comment" do
    expected = [
      {:try, ""},
      :deactivate]
    command = """
    bors try
    bors r-
    """
    assert expected == Command.parse(command)
  end

  test "accept the try command with more argumentation" do
    assert [{:try, " --layout --script"}] ==
      Command.parse("bors try --layout --script")
  end

  test "do not accept the command with a prefix" do
    assert [] == Command.parse("Xbors tryZ")
  end

  test "running ping command should post comment", %{proj: proj} do
    GitHub.ServerMock.put_state(%{
      {{:installation, 91}, 14} => %{
        branches: %{},
        comments: %{1 => []},
        statuses: %{}
      }
    })

    c = %Command{
      project: proj,
      commenter: "commenter",
      comment: "bors ping",
      pr_xref: 1
    }
    Command.run(c, :ping)
    assert GitHub.ServerMock.get_state() == %{
      {{:installation, 91}, 14} => %{
        branches: %{},
        comments: %{1 => ["pong"]},
        statuses: %{}
      }
    }
  end
end


Made some fields optional for chargers (#41)

defmodule CgratesWebJsonapi.TariffPlans.TpCharger do
  use Ecto.Schema
  import Ecto.Changeset
  use EctoConditionals, repo: CgratesWebJsonapi.Repo

  @attributes ~w[tpid tenant custom_id attribute_ids filter_ids run_id activation_interval weight]a

  use CgratesWebJsonapi.CsvImport, module: __MODULE__, attributes: @attributes

  @primary_key {:pk, :id, autogenerate: true}
  @derive {Phoenix.Param, key: :pk}
  schema "tp_chargers" do
    field :id, :integer, source: :pk
    field :tpid, :string
    field :tenant, :string
    field :custom_id, :string, source: :id
    field :filter_ids, :string, default: ""
    field :activation_interval, :string, default: ""
    field :run_id, :string
    field :attribute_ids, :string, default: ""
    field :weight, :decimal

    field :created_at, :naive_datetime
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, @attributes)
    |> validate_required(~w[tpid tenant custom_id run_id weight]a)
    |> validate_length(:tpid, max: 64)
    |> validate_length(:tenant, max: 64)
    |> validate_length(:custom_id, max: 64)
    |> validate_length(:run_id, max: 64)
    |> validate_length(:filter_ids, max: 64)
    |> validate_length(:attribute_ids, max: 64)
    |> validate_length(:activation_interval, max: 64)
  end
end

defmodule CgratesWebJsonapi.TariffPlans.TpCharger do
  use Ecto.Schema
  import Ecto.Changeset
  use EctoConditionals, repo: CgratesWebJsonapi.Repo

  @attributes ~w[tpid tenant custom_id attribute_ids filter_ids run_id activation_interval weight]a

  use CgratesWebJsonapi.CsvImport, module: __MODULE__, attributes: @attributes

  @primary_key {:pk, :id, autogenerate: true}
  @derive {Phoenix.Param, key: :pk}
  schema "tp_chargers" do
    field :id, :integer, source: :pk
    field :tpid, :string
    field :tenant, :string
    field :custom_id, :string, source: :id
    field :filter_ids, :string
    field :activation_interval, :string
    field :run_id, :string
    field :attribute_ids, :string
    field :weight, :decimal

    field :created_at, :naive_datetime
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, @attributes)
    |> validate_required(@attributes)
    |> validate_length(:tpid, max: 64)
    |> validate_length(:tenant, max: 64)
    |> validate_length(:custom_id, max: 64)
    |> validate_length(:run_id, max: 64)
    |> validate_length(:filter_ids, max: 64)
    |> validate_length(:attribute_ids, max: 64)
    |> validate_length(:activation_interval, max: 64)
  end
end


structs for days

structs for days

defmodule Sequence.Manager do
  @moduledoc """
    This Module is a state machine that tracks a sequence thru its lifecycle.
  """
  use GenServer
  require Logger

  def init(%Sequence{} = sequence) do
    Process.flag(:trap_exit, true)
    {:ok, pid} = Sequence.VM.start_link(sequence)
    {:ok, %{current: pid, global_vars: %{}, log: []}}
  end

  def start_link(sequence) do
    GenServer.start_link(__MODULE__, sequence, name: __MODULE__)
  end

  def handle_call(:e_stop, _from, %{current: nil, global_vars: _, log: _}) do
    {:reply, :ok, %{current: nil, global_vars: %{}, log: []}}
  end

  # no sequences running, no sequences in list.
  def handle_call({:add, %Sequence{} = seq}, _from,
    %{current: nil, global_vars: globals, log: []})
  do
    {:ok, pid} = Sequence.VM.start_link(seq)
    {:reply, "starting sequence", %{current: pid, global_vars: globals, log: []}}
  end

  # Add a new sequence to the log when there are no other sequences running.
  def handle_call({:add, %Sequence{} = seq}, _from,
    %{current: nil, global_vars: globals, log: log})
  do
    {:ok, pid} = Sequence.VM.start_link(seq)
    {:reply, "starting sequence", %{current: pid, global_vars: globals, log: log}}
  end

  # Add a new sequence to the log
  def handle_call({:add, %Sequence{} = seq}, _from, %{current: current, global_vars: globals, log: log})
  when is_pid(current) do
    {:reply, "queueing sequence", %{current: current, global_vars: globals, log: [log | seq]}}
  end

  def handle_call({:pause, pid}, _from, %{current: current, global_vars: globals, log: more}) do
    cond do
      Process.alive?(pid) ->
        GenServer.call(current, :pause)
        {:reply, :ok, %{current: nil, global_vars: globals, log: [ pid | more ]}}
    end
  end

  def handle_call({:resume, pid}, _from, %{current: _current, global_vars: globals, log: more}) do
    cond do
      Process.alive?(pid) ->
        GenServer.cast(pid, :resume)
        {:reply, :ok, %{current: pid, global_vars: globals, log: more}}
    end
  end

  def handle_info({:done, pid, sequence},
    %{current: _current, global_vars: globals, log: []})
  when is_pid(pid) do
    if(Process.alive?(pid)) do
      GenServer.stop(pid, :normal)
    end
    RPC.MessageHandler.log("No more sub sequences.", [], [__MODULE__])
    send(Farmbot.Scheduler, {:done, {:sequence, self(), sequence}})
    {:noreply, %{current: nil, global_vars: globals, log: [] }}
  end

  def handle_info({:done, pid, _sequence}, %{current: _current, global_vars: globals, log: log})
  when is_pid(pid) do
    if(Process.alive?(pid)) do
      GenServer.stop(pid, :normal)
    end
    RPC.MessageHandler.log("Running next sub sequence", [], [__MODULE__])
    next = List.first(log)
    cond do
      is_nil(next) -> {:noreply, %{current: nil, global_vars: globals, log: []}}
      is_map(next) ->
          {:ok, next_seq} = Sequence.VM.start_link(next)
          {:noreply, %{current: next_seq, global_vars: globals, log: log -- [next]}}
      is_pid(next) ->
        GenServer.cast(next, :resume)
        {:noreply, %{current: next, global_vars: globals, log: log -- [next]}}
    end
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:noreply, state}
  end

  def handle_info({:EXIT, pid, reason}, state)
  when pid == state do
    msg = "Sequence died of unnatural causes: #{inspect reason}"
    Logger.debug(msg)
    RPC.MessageHandler.log(msg, [:error_toast], [__MODULE__])
    handle_info({:done, pid, %{}}, state)
  end

  def terminate(:normal, _state) do
    Logger.debug("Sequence Manager shutting down")
  end

  def terminate(reason, _state) do
    Logger.debug("Sequence Manager died unnaturally: #{inspect reason}")
  end
end

defmodule Sequence.Manager do
  @moduledoc """
    This Module is a state machine that tracks a sequence thru its lifecycle.
  """
  use GenServer
  require Logger

  def init(%Sequence{} = sequence) do
    Process.flag(:trap_exit, true)
    {:ok, pid} = Sequence.VM.start_link(sequence)
    {:ok, %{current: pid, global_vars: %{}, log: []}}
  end

  def start_link(sequence) do
    GenServer.start_link(__MODULE__, sequence, name: __MODULE__)
  end

  def handle_call(:e_stop, _from, %{current: nil, global_vars: _, log: _}) do
    {:reply, :ok, %{current: nil, global_vars: %{}, log: []}}
  end

  # no sequences running, no sequences in list.
  def handle_call({:add, seq}, _from, %{current: nil, global_vars: globals, log: []})
  when is_map(seq) do
    {:ok, pid} = Sequence.VM.start_link(seq)
    {:reply, "starting sequence", %{current: pid, global_vars: globals, log: []}}
  end

  # Add a new sequence to the log when there are no other sequences running.
  def handle_call({:add, seq}, _from, %{current: nil, global_vars: globals, log: log})
  when is_map(seq) do
    {:ok, pid} = Sequence.VM.start_link(seq)
    {:reply, "starting sequence", %{current: pid, global_vars: globals, log: log}}
  end

  # Add a new sequence to the log
  def handle_call({:add, seq}, _from, %{current: current, global_vars: globals, log: log})
  when is_map(seq) and is_pid(current) do
    {:reply, "queueing sequence", %{current: current, global_vars: globals, log: [log | seq]}}
  end

  def handle_call({:pause, pid}, _from, %{current: current, global_vars: globals, log: more}) do
    cond do
      Process.alive?(pid) ->
        GenServer.call(current, :pause)
        {:reply, :ok, %{current: nil, global_vars: globals, log: [ pid | more ]}}
    end
  end

  def handle_call({:resume, pid}, _from, %{current: _current, global_vars: globals, log: more}) do
    cond do
      Process.alive?(pid) ->
        GenServer.cast(pid, :resume)
        {:reply, :ok, %{current: pid, global_vars: globals, log: more}}
    end
  end

  def handle_info({:done, pid, sequence}, %{current: _current, global_vars: globals, log: []})
  when is_pid(pid) do
    if(Process.alive?(pid)) do
      GenServer.stop(pid, :normal)
    end
    RPC.MessageHandler.log("No more sub sequences.", [], ["Sequence.VM"])
    send(Farmbot.Scheduler, {:done, {:sequence, self(), sequence}})
    {:noreply, %{current: nil, global_vars: globals, log: [] }}
  end

  def handle_info({:done, pid, _sequence}, %{current: _current, global_vars: globals, log: log})
  when is_pid(pid) do
    if(Process.alive?(pid)) do
      GenServer.stop(pid, :normal)
    end
    RPC.MessageHandler.log("Running next sub sequence", [], ["Sequence.VM"])
    next = List.first(log)
    cond do
      is_nil(next) -> {:noreply, %{current: nil, global_vars: globals, log: []}}
      is_map(next) ->
          {:ok, next_seq} = Sequence.VM.start_link(next)
          {:noreply, %{current: next_seq, global_vars: globals, log: log -- [next]}}
      is_pid(next) ->
        GenServer.cast(next, :resume)
        {:noreply, %{current: next, global_vars: globals, log: log -- [next]}}
    end
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:noreply, state}
  end

  def handle_info({:EXIT, pid, reason}, state)
  when pid == state do
    msg = "Sequence died of unnatural causes: #{inspect reason}"
    Logger.debug(msg)
    RPC.MessageHandler.log(msg, [:error_toast], ["Sequence.VM"])
    handle_info({:done, pid, %{}}, state)
  end

  def terminate(:normal, _state) do
    Logger.debug("Sequence Manager shutting down")
  end

  def terminate(reason, _state) do
    Logger.debug("Sequence Manager died unnaturally: #{inspect reason}")
  end
end


on_migrate override fix

on_migrate override fix

#-------------------------------------------------------------------------------
# Author: Keith Brings
# Copyright (C) 2019 Noizu Labs, Inc. All rights reserved.
#-------------------------------------------------------------------------------

defmodule Noizu.SimplePool.V2.InnerStateBehaviour do
  @moduledoc """
    The method provides scaffolding for Pool Worker Entities.  Such as support for calls such as shutdown, different init strategies, etc.

    @note this module is currently a duplicate of the V1 implementation
    @todo InnerStateBehaviour and WorkerBehaviour should be combined while moving the split between the Pool.Worker and the actual entity.
  """
  require Logger
  #@callback call_forwarding(call :: any, context :: any, from :: any,  state :: any, outer_state :: any) :: {atom, reply :: any, state :: any}
  @callback fetch(this :: any, options :: any, context :: any) :: {:reply, this :: any, this :: any}

  @callback load(ref :: any) ::  any
  @callback load(ref :: any, context :: any) :: any
  @callback load(ref :: any, context :: any, options :: any) :: any

  @callback terminate_hook(reason :: any,  Noizu.SimplePool.Worker.State.t) :: {:ok, Noizu.SimplePool.Worker.State.t}
  @callback shutdown(Noizu.SimplePool.Worker.State.t, context :: any, options :: any, from :: any) :: {:ok | :wait, Noizu.SimplePool.Worker.State.t}
  @callback worker_refs(any, any, any) :: any | nil

  @callback supervisor_hint(ref :: any) :: integer

  alias Noizu.ElixirCore.OptionSettings
  alias Noizu.ElixirCore.OptionValue
  alias Noizu.ElixirCore.OptionList

  @required_methods ([:call_forwarding, :load])
  @provided_methods ([:call_forwarding_catchall, :fetch, :shutdown, :terminate_hook, :get_direct_link!, :worker_refs, :ping!, :kill!, :crash!, :health_check!, :migrate_shutdown, :on_migrate, :transfer, :save!, :reload!, :supervisor_hint])

  @methods (@required_methods ++ @provided_methods)
  @features ([:auto_identifier, :lazy_load, :inactivitiy_check, :s_redirect])
  @default_features ([:lazy_load, :s_redirect, :inactivity_check])

  def prepare_options(options) do
    settings = %OptionSettings{
      option_settings: %{
        pool: %OptionValue{option: :pool, required: true},
        features: %OptionList{option: :features, default: Application.get_env(:noizu_simple_pool, :default_features, @default_features), valid_members: @features, membership_set: false},
        only: %OptionList{option: :only, default: @provided_methods, valid_members: @methods, membership_set: true},
        override: %OptionList{option: :override, default: [], valid_members: @methods, membership_set: true},
      }
    }
    initial = OptionSettings.expand(settings, options)
    modifications = Map.put(initial.effective_options, :required, List.foldl(@methods, %{}, fn(x, acc) -> Map.put(acc, x, initial.effective_options.only[x] && !initial.effective_options.override[x]) end))
    %OptionSettings{initial| effective_options: Map.merge(initial.effective_options, modifications)}
  end

  def default_terminate_hook(server, reason, state) do
    case reason do
      {:shutdown, {:migrate, _ref, _, :to, _}} ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :migrate, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
      {:shutdown, :migrate} ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :migrate, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
      {:shutdown, _details} ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :shutdown, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
      _ ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :terminate, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
    end
    :ok
  end

  defmacro __using__(options) do
    option_settings = prepare_options(options)
    options = option_settings.effective_options
    required = options.required
    pool = options.pool
    message_processing_provider = Noizu.SimplePool.V2.MessageProcessingBehaviour.DefaultProvider
    quote do
      import unquote(__MODULE__)
      @behaviour Noizu.SimplePool.V2.InnerStateBehaviour
      @base (unquote(Macro.expand(pool, __CALLER__)))
      @worker (Module.concat([@base, "Worker"]))
      @worker_supervisor (Module.concat([@base, "WorkerSupervisor"]))
      @server (Module.concat([@base, "Server"]))
      @pool_supervisor (Module.concat([@base, "PoolSupervisor"]))
      @simple_pool_group ({@base, @worker, @worker_supervisor, @server, @pool_supervisor})

      use unquote(message_processing_provider), unquote(option_settings)

      alias Noizu.SimplePool.Worker.Link

      #---------------------------------
      #
      #---------------------------------
      def supervisor_hint(ref) do
        case id(ref) do
          v when is_integer(v) -> v
          {a, v} when is_atom(a) and is_integer(v) -> v # To allow for a common id pattern in a number of noizu related projects.
        end
      end

      #---------------------------------
      #
      #---------------------------------
      def get_direct_link!(ref, context), do: @server.router().get_direct_link!(ref, context)

      #-------------------------------------------------------------------------------
      # Outer Context - Exceptions
      #-------------------------------------------------------------------------------
      def reload!(%Noizu.SimplePool.Worker.State{} = state, context, options) do
        case load(state.worker_ref, context, options) do
          nil -> {:reply, :error, state}
          inner_state ->
            {:reply, :ok, %Noizu.SimplePool.Worker.State{state| initialized: true, inner_state: inner_state, last_activity: :os.system_time(:seconds)}}
        end
      end

      #---------------------------------
      #
      #---------------------------------
      def save!(outer_state, _context, _options) do
        Logger.warn("#{__MODULE__}.save method not implemented.")
        {:reply, {:error, :implementation_required}, outer_state}
      end

      #-------------------------------------------------------------------------------
      # Message Handlers
      #-------------------------------------------------------------------------------

      #-----------------------------
      # fetch!/4,5
      #-----------------------------
      def fetch!(state, args, from, context), do: fetch!(state, args, from, context, nil)
      def fetch!(state, _args, _from, _context, _options), do: {:reply, state, state}

      #---------------------------------
      #
      #---------------------------------
      def wake!(%__MODULE__{} = this, command, from, context), do: wake!(this, command, from, context, nil)
      def wake!(%__MODULE__{} = this, _command, _from, _context, _options), do: {:reply, this, this}

      #----------------------------------
      # routing
      #----------------------------------

      #------------------------------------------------------------------------
      # Infrastructure provided call router
      #------------------------------------------------------------------------

      #---------------------------------
      #
      #---------------------------------
      def call_router_internal__default({:passive, envelope}, from, state), do: call_router_internal__default(envelope, from, state)
      def call_router_internal__default({:spawn, envelope}, from, state), do: call_router_internal__default(envelope, from, state)
      def call_router_internal__default(envelope, from, state) do
        case envelope do
          # fetch!
          {:s, {:fetch!, args}, context} -> fetch!(state, args, from, context)
          {:s, {:fetch!, args, opts}, context} -> fetch!(state, args, from, context, opts)

          # wake!
          {:s, {:wake!, args}, context} -> wake!(state, args, from, context)
          {:s, {:wake!, args, opts}, context} -> wake!(state, args, from, context, opts)

          _ -> nil
        end
      end

      #---------------------------------
      #
      #---------------------------------
      def call_router_internal(envelope, from, state), do: call_router_internal__default(envelope, from, state)

      #----------------------------
      #
      #----------------------------
      def cast_router_internal__default(envelope, state) do
        r = call_router_internal(envelope, :cast, state)
        r && as_cast(r)
      end

      #----------------------------
      #
      #----------------------------
      def cast_router_internal(envelope, state), do: cast_router_internal__default(envelope, state)

      #----------------------------
      #
      #----------------------------
      def info_router_internal__default(envelope, state) do
        nil
      end
      #----------------------------
      #
      #----------------------------
      def info_router_internal(envelope, state), do: info_router_internal__default(envelope, state)

      #----------------------------------
      #
      #----------------------------------
      def shutdown(%Noizu.SimplePool.Worker.State{} = state, _context \\ nil, options \\ nil, _from \\ nil), do: {:ok, state}

      #---------------------------------
      #
      #---------------------------------
      def migrate_shutdown(%Noizu.SimplePool.Worker.State{} = state, _context \\ nil), do: {:ok, state}

      #---------------------------------
      #
      #---------------------------------
      def on_migrate(_rebase, %Noizu.SimplePool.Worker.State{} = state, _context \\ nil, _options \\ nil), do: {:ok, state}

      #---------------------------------
      #
      #---------------------------------
      def terminate_hook(reason, state), do: default_terminate_hook(@server, reason, state)

      #---------------------------------
      #
      #---------------------------------
      def worker_refs(_context, _options, _state), do: nil

      #---------------------------------
      #
      #---------------------------------
      def transfer(ref, transfer_state, _context \\ nil), do: {true, transfer_state}

      defoverridable [
        supervisor_hint: 1,
        get_direct_link!: 2,
        reload!: 3,
        save!: 3,
        fetch!: 4,
        fetch!: 5,
        wake!: 4,
        wake!: 5,

        call_router_internal__default: 3,
        call_router_internal: 3,
        cast_router_internal__default: 2,
        cast_router_internal: 2,
        info_router_internal__default: 2,
        info_router_internal: 2,

        shutdown: 1,
        shutdown: 2,
        shutdown: 3,
        shutdown: 4,

        migrate_shutdown: 1,
        migrate_shutdown: 2,

        on_migrate: 2,
        on_migrate: 3,
        on_migrate: 4,

        terminate_hook: 2,
        worker_refs: 3,
        transfer: 2,
        transfer: 3,
      ]

    end # end quote
  end #end defmacro __using__(options)

end # end module

#-------------------------------------------------------------------------------
# Author: Keith Brings
# Copyright (C) 2019 Noizu Labs, Inc. All rights reserved.
#-------------------------------------------------------------------------------

defmodule Noizu.SimplePool.V2.InnerStateBehaviour do
  @moduledoc """
    The method provides scaffolding for Pool Worker Entities.  Such as support for calls such as shutdown, different init strategies, etc.

    @note this module is currently a duplicate of the V1 implementation
    @todo InnerStateBehaviour and WorkerBehaviour should be combined while moving the split between the Pool.Worker and the actual entity.
  """
  require Logger
  #@callback call_forwarding(call :: any, context :: any, from :: any,  state :: any, outer_state :: any) :: {atom, reply :: any, state :: any}
  @callback fetch(this :: any, options :: any, context :: any) :: {:reply, this :: any, this :: any}

  @callback load(ref :: any) ::  any
  @callback load(ref :: any, context :: any) :: any
  @callback load(ref :: any, context :: any, options :: any) :: any

  @callback terminate_hook(reason :: any,  Noizu.SimplePool.Worker.State.t) :: {:ok, Noizu.SimplePool.Worker.State.t}
  @callback shutdown(Noizu.SimplePool.Worker.State.t, context :: any, options :: any, from :: any) :: {:ok | :wait, Noizu.SimplePool.Worker.State.t}
  @callback worker_refs(any, any, any) :: any | nil

  @callback supervisor_hint(ref :: any) :: integer

  alias Noizu.ElixirCore.OptionSettings
  alias Noizu.ElixirCore.OptionValue
  alias Noizu.ElixirCore.OptionList

  @required_methods ([:call_forwarding, :load])
  @provided_methods ([:call_forwarding_catchall, :fetch, :shutdown, :terminate_hook, :get_direct_link!, :worker_refs, :ping!, :kill!, :crash!, :health_check!, :migrate_shutdown, :on_migrate, :transfer, :save!, :reload!, :supervisor_hint])

  @methods (@required_methods ++ @provided_methods)
  @features ([:auto_identifier, :lazy_load, :inactivitiy_check, :s_redirect])
  @default_features ([:lazy_load, :s_redirect, :inactivity_check])

  def prepare_options(options) do
    settings = %OptionSettings{
      option_settings: %{
        pool: %OptionValue{option: :pool, required: true},
        features: %OptionList{option: :features, default: Application.get_env(:noizu_simple_pool, :default_features, @default_features), valid_members: @features, membership_set: false},
        only: %OptionList{option: :only, default: @provided_methods, valid_members: @methods, membership_set: true},
        override: %OptionList{option: :override, default: [], valid_members: @methods, membership_set: true},
      }
    }
    initial = OptionSettings.expand(settings, options)
    modifications = Map.put(initial.effective_options, :required, List.foldl(@methods, %{}, fn(x, acc) -> Map.put(acc, x, initial.effective_options.only[x] && !initial.effective_options.override[x]) end))
    %OptionSettings{initial| effective_options: Map.merge(initial.effective_options, modifications)}
  end

  def default_terminate_hook(server, reason, state) do
    case reason do
      {:shutdown, {:migrate, _ref, _, :to, _}} ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :migrate, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
      {:shutdown, :migrate} ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :migrate, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
      {:shutdown, _details} ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :shutdown, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
      _ ->
        server.worker_management().unregister!(state.worker_ref, Noizu.ElixirCore.CallingContext.system(%{}))
        #PRI-0 - disabled until rate limit available - server.worker_management().record_event!(state.worker_ref, :terminate, reason, Noizu.ElixirCore.CallingContext.system(%{}), %{})
        reason
    end
    :ok
  end

  defmacro __using__(options) do
    option_settings = prepare_options(options)
    options = option_settings.effective_options
    required = options.required
    pool = options.pool
    message_processing_provider = Noizu.SimplePool.V2.MessageProcessingBehaviour.DefaultProvider
    quote do
      import unquote(__MODULE__)
      @behaviour Noizu.SimplePool.V2.InnerStateBehaviour
      @base (unquote(Macro.expand(pool, __CALLER__)))
      @worker (Module.concat([@base, "Worker"]))
      @worker_supervisor (Module.concat([@base, "WorkerSupervisor"]))
      @server (Module.concat([@base, "Server"]))
      @pool_supervisor (Module.concat([@base, "PoolSupervisor"]))
      @simple_pool_group ({@base, @worker, @worker_supervisor, @server, @pool_supervisor})

      use unquote(message_processing_provider), unquote(option_settings)

      alias Noizu.SimplePool.Worker.Link

      #---------------------------------
      #
      #---------------------------------
      def supervisor_hint(ref) do
        case id(ref) do
          v when is_integer(v) -> v
          {a, v} when is_atom(a) and is_integer(v) -> v # To allow for a common id pattern in a number of noizu related projects.
        end
      end

      #---------------------------------
      #
      #---------------------------------
      def get_direct_link!(ref, context), do: @server.router().get_direct_link!(ref, context)

      #-------------------------------------------------------------------------------
      # Outer Context - Exceptions
      #-------------------------------------------------------------------------------
      def reload!(%Noizu.SimplePool.Worker.State{} = state, context, options) do
        case load(state.worker_ref, context, options) do
          nil -> {:reply, :error, state}
          inner_state ->
            {:reply, :ok, %Noizu.SimplePool.Worker.State{state| initialized: true, inner_state: inner_state, last_activity: :os.system_time(:seconds)}}
        end
      end

      #---------------------------------
      #
      #---------------------------------
      def save!(outer_state, _context, _options) do
        Logger.warn("#{__MODULE__}.save method not implemented.")
        {:reply, {:error, :implementation_required}, outer_state}
      end

      #-------------------------------------------------------------------------------
      # Message Handlers
      #-------------------------------------------------------------------------------

      #-----------------------------
      # fetch!/4,5
      #-----------------------------
      def fetch!(state, args, from, context), do: fetch!(state, args, from, context, nil)
      def fetch!(state, _args, _from, _context, _options), do: {:reply, state, state}

      #---------------------------------
      #
      #---------------------------------
      def wake!(%__MODULE__{} = this, command, from, context), do: wake!(this, command, from, context, nil)
      def wake!(%__MODULE__{} = this, _command, _from, _context, _options), do: {:reply, this, this}

      #----------------------------------
      # routing
      #----------------------------------

      #------------------------------------------------------------------------
      # Infrastructure provided call router
      #------------------------------------------------------------------------

      #---------------------------------
      #
      #---------------------------------
      def call_router_internal__default({:passive, envelope}, from, state), do: call_router_internal__default(envelope, from, state)
      def call_router_internal__default({:spawn, envelope}, from, state), do: call_router_internal__default(envelope, from, state)
      def call_router_internal__default(envelope, from, state) do
        case envelope do
          # fetch!
          {:s, {:fetch!, args}, context} -> fetch!(state, args, from, context)
          {:s, {:fetch!, args, opts}, context} -> fetch!(state, args, from, context, opts)

          # wake!
          {:s, {:wake!, args}, context} -> wake!(state, args, from, context)
          {:s, {:wake!, args, opts}, context} -> wake!(state, args, from, context, opts)

          _ -> nil
        end
      end

      #---------------------------------
      #
      #---------------------------------
      def call_router_internal(envelope, from, state), do: call_router_internal__default(envelope, from, state)

      #----------------------------
      #
      #----------------------------
      def cast_router_internal__default(envelope, state) do
        r = call_router_internal(envelope, :cast, state)
        r && as_cast(r)
      end

      #----------------------------
      #
      #----------------------------
      def cast_router_internal(envelope, state), do: cast_router_internal__default(envelope, state)

      #----------------------------
      #
      #----------------------------
      def info_router_internal__default(envelope, state) do
        nil
      end
      #----------------------------
      #
      #----------------------------
      def info_router_internal(envelope, state), do: info_router_internal__default(envelope, state)

      #----------------------------------
      #
      #----------------------------------
      def shutdown(%Noizu.SimplePool.Worker.State{} = state, _context \\ nil, options \\ nil, _from \\ nil), do: {:ok, state}

      #---------------------------------
      #
      #---------------------------------
      def migrate_shutdown(%Noizu.SimplePool.Worker.State{} = state, _context \\ nil), do: {:ok, state}

      #---------------------------------
      #
      #---------------------------------
      def on_migrate(_rebase, %Noizu.SimplePool.Worker.State{} = state, _context \\ nil, _options \\ nil), do: {:ok, state}

      #---------------------------------
      #
      #---------------------------------
      def terminate_hook(reason, state), do: default_terminate_hook(@server, reason, state)

      #---------------------------------
      #
      #---------------------------------
      def worker_refs(_context, _options, _state), do: nil

      #---------------------------------
      #
      #---------------------------------
      def transfer(ref, transfer_state, _context \\ nil), do: {true, transfer_state}

      defoverridable [
        supervisor_hint: 1,
        get_direct_link!: 2,
        reload!: 3,
        save!: 3,
        fetch!: 4,
        fetch!: 5,
        wake!: 4,
        wake!: 5,

        call_router_internal__default: 3,
        call_router_internal: 3,
        cast_router_internal__default: 2,
        cast_router_internal: 2,
        info_router_internal__default: 2,
        info_router_internal: 2,

        shutdown: 1,
        shutdown: 2,
        shutdown: 3,
        shutdown: 4,

        migrate_shutdown: 1,
        migrate_shutdown: 2,

        on_migrate: 1,
        on_migrate: 2,

        terminate_hook: 2,
        worker_refs: 3,
        transfer: 2,
        transfer: 3,
      ]

    end # end quote
  end #end defmacro __using__(options)

end # end module


Have to disable colourised output in vim for now


Use both Code.get_docs/2 and Code.fetch_docs/1

defmodule Tesla.BuilderTest do
  use ExUnit.Case

  describe "Compilation" do
    defmodule TestClientPlug do
      use Tesla.Builder

      @attr "value"

      plug FirstMiddleware, @attr
      plug SecondMiddleware, options: :are, fun: 1
      plug ThirdMiddleware
      plug fn env, _next -> env end

      def new do
        Tesla.Builder.client(
          [
            FirstMiddleware,
            {SecondMiddleware, options: :are, fun: 1},
            fn env, _next -> env end
          ],
          []
        )
      end

      def new(middlewares) do
        Tesla.Builder.client(middlewares, [])
      end
    end

    defmodule TestClientModule do
      use Tesla.Builder
      adapter TheAdapter, hello: "world"
    end

    defmodule TestClientAnon do
      use Tesla.Builder
      adapter fn env -> env end
    end

    test "generate __middleware__/0" do
      assert [
               {FirstMiddleware, :call, ["value"]},
               {SecondMiddleware, :call, [[options: :are, fun: 1]]},
               {ThirdMiddleware, :call, [[]]},
               {:fn, fun}
             ] = TestClientPlug.__middleware__()

      assert is_function(fun)
    end

    test "generate __adapter__/0 - adapter not set" do
      assert TestClientPlug.__adapter__() == nil
    end

    test "generate __adapter__/0 - adapter as module" do
      assert TestClientModule.__adapter__() == {TheAdapter, :call, [[hello: "world"]]}
    end

    test "generate __adapter__/0 - adapter as anonymous function" do
      assert {:fn, fun} = TestClientAnon.__adapter__()
      assert is_function(fun)
    end

    test "dynamic client" do
      client = TestClientPlug.new()

      assert [
               {FirstMiddleware, :call, [[]]},
               {SecondMiddleware, :call, [[options: :are, fun: 1]]},
               {:fn, fun}
             ] = client.pre

      assert is_function(fun)
    end

    test "client from module attribute" do
      middlewares = [
        FirstMiddleware,
        {SecondMiddleware, options: :are, fun: 1},
        fn env, _next -> env end
      ]

      client = TestClientPlug.new(middlewares)

      assert [
               {FirstMiddleware, :call, [[]]},
               {SecondMiddleware, :call, [[options: :are, fun: 1]]},
               {:fn, fun}
             ] = client.pre

      assert is_function(fun)
    end
  end

  describe "Function generation" do
    defmodule TestClient do
      use Tesla.Builder
    end

    test "generate multiple variants for HTTP methods" do
      assert function_exported?(TestClient, :get, 1)
      assert function_exported?(TestClient, :get, 2)
      assert function_exported?(TestClient, :get, 3)

      assert function_exported?(TestClient, :post, 2)
      assert function_exported?(TestClient, :post, 3)
      assert function_exported?(TestClient, :post, 4)
    end

    test "generate bang variants for HTTP methods" do
      assert function_exported?(TestClient, :get!, 1)
      assert function_exported?(TestClient, :get!, 2)
      assert function_exported?(TestClient, :get!, 3)

      assert function_exported?(TestClient, :post!, 2)
      assert function_exported?(TestClient, :post!, 3)
      assert function_exported?(TestClient, :post!, 4)
    end
  end

  describe ":only/:except options" do
    defmodule OnlyGetClient do
      use Tesla.Builder, only: [:get]
    end

    defmodule ExceptDeleteClient do
      use Tesla.Builder, except: ~w(delete)a
    end

    @http_verbs ~w(head get delete trace options post put patch)a

    test "limit generated functions (only)" do
      functions = OnlyGetClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
      assert :get in functions
      refute Enum.any?(@http_verbs -- [:get], &(&1 in functions))
    end

    test "limit generated functions (except)" do
      functions = ExceptDeleteClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
      refute :delete in functions
      assert Enum.all?(@http_verbs -- [:delete], &(&1 in functions))
    end
  end

  describe ":docs option" do
    # Code.fetch_docs/1 requires .beam file of given module to exist in file system
    # See test/support/docs.ex file for definitions of TeslaDocsTest.* modules

    if Version.match?(System.version(), ">= 1.7.0-rc.0") do
      # Use Code.fetch_docs/1 for elixir 1.7+
      test "generate docs by default" do
        {:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(TeslaDocsTest.Default)
        assert {_, _, _, doc, _} = Enum.find(docs, &match?({{:function, :get, 3}, _, _, _, _}, &1))
        assert doc != :hidden
      end

      test "do not generate docs for HTTP methods when docs: false" do
        {:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(TeslaDocsTest.NoDocs)

        assert {_, _, _, doc, _} = Enum.find(docs, &match?({{:function, :get, 3}, _, _, _, _}, &1))
        assert doc == :hidden

        assert {_, _, _, doc, _} = Enum.find(docs, &match?({{:function, :custom, 1}, _, _, _, _}, &1))
        assert doc["en"] =~ ~r/something/
      end
    else
      # Use Code.get_docs/2 for elixir <1.7
      test "generate docs by default" do
        docs = Code.get_docs(TeslaDocsTest.Default, :docs)
        assert {_, _, _, _, doc} = Enum.find(docs, &match?({{:get, 3}, _, :def, _, _}, &1))
        assert doc != false
      end

      test "do not generate docs for HTTP methods when docs: false" do
        docs = Code.get_docs(TeslaDocsTest.NoDocs, :docs)
        assert {_, _, _, _, false} = Enum.find(docs, &match?({{:get, 3}, _, :def, _, _}, &1))
        assert {_, _, _, _, doc} = Enum.find(docs, &match?({{:custom, 1}, _, :def, _, _}, &1))
        assert doc =~ ~r/something/
      end
    end
  end
end

defmodule Tesla.BuilderTest do
  use ExUnit.Case

  describe "Compilation" do
    defmodule TestClientPlug do
      use Tesla.Builder

      @attr "value"

      plug FirstMiddleware, @attr
      plug SecondMiddleware, options: :are, fun: 1
      plug ThirdMiddleware
      plug fn env, _next -> env end

      def new do
        Tesla.Builder.client(
          [
            FirstMiddleware,
            {SecondMiddleware, options: :are, fun: 1},
            fn env, _next -> env end
          ],
          []
        )
      end

      def new(middlewares) do
        Tesla.Builder.client(middlewares, [])
      end
    end

    defmodule TestClientModule do
      use Tesla.Builder
      adapter TheAdapter, hello: "world"
    end

    defmodule TestClientAnon do
      use Tesla.Builder
      adapter fn env -> env end
    end

    test "generate __middleware__/0" do
      assert [
               {FirstMiddleware, :call, ["value"]},
               {SecondMiddleware, :call, [[options: :are, fun: 1]]},
               {ThirdMiddleware, :call, [[]]},
               {:fn, fun}
             ] = TestClientPlug.__middleware__()

      assert is_function(fun)
    end

    test "generate __adapter__/0 - adapter not set" do
      assert TestClientPlug.__adapter__() == nil
    end

    test "generate __adapter__/0 - adapter as module" do
      assert TestClientModule.__adapter__() == {TheAdapter, :call, [[hello: "world"]]}
    end

    test "generate __adapter__/0 - adapter as anonymous function" do
      assert {:fn, fun} = TestClientAnon.__adapter__()
      assert is_function(fun)
    end

    test "dynamic client" do
      client = TestClientPlug.new()

      assert [
               {FirstMiddleware, :call, [[]]},
               {SecondMiddleware, :call, [[options: :are, fun: 1]]},
               {:fn, fun}
             ] = client.pre

      assert is_function(fun)
    end

    test "client from module attribute" do
      middlewares = [
        FirstMiddleware,
        {SecondMiddleware, options: :are, fun: 1},
        fn env, _next -> env end
      ]

      client = TestClientPlug.new(middlewares)

      assert [
               {FirstMiddleware, :call, [[]]},
               {SecondMiddleware, :call, [[options: :are, fun: 1]]},
               {:fn, fun}
             ] = client.pre

      assert is_function(fun)
    end
  end

  describe "Function generation" do
    defmodule TestClient do
      use Tesla.Builder
    end

    test "generate multiple variants for HTTP methods" do
      assert function_exported?(TestClient, :get, 1)
      assert function_exported?(TestClient, :get, 2)
      assert function_exported?(TestClient, :get, 3)

      assert function_exported?(TestClient, :post, 2)
      assert function_exported?(TestClient, :post, 3)
      assert function_exported?(TestClient, :post, 4)
    end

    test "generate bang variants for HTTP methods" do
      assert function_exported?(TestClient, :get!, 1)
      assert function_exported?(TestClient, :get!, 2)
      assert function_exported?(TestClient, :get!, 3)

      assert function_exported?(TestClient, :post!, 2)
      assert function_exported?(TestClient, :post!, 3)
      assert function_exported?(TestClient, :post!, 4)
    end
  end

  describe ":only/:except options" do
    defmodule OnlyGetClient do
      use Tesla.Builder, only: [:get]
    end

    defmodule ExceptDeleteClient do
      use Tesla.Builder, except: ~w(delete)a
    end

    @http_verbs ~w(head get delete trace options post put patch)a

    test "limit generated functions (only)" do
      functions = OnlyGetClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
      assert :get in functions
      refute Enum.any?(@http_verbs -- [:get], &(&1 in functions))
    end

    test "limit generated functions (except)" do
      functions = ExceptDeleteClient.__info__(:functions) |> Keyword.keys() |> Enum.uniq()
      refute :delete in functions
      assert Enum.all?(@http_verbs -- [:delete], &(&1 in functions))
    end
  end

  describe ":docs option" do
    # Code.fetch_docs/1 requires .beam file of given module to exist in file system
    # See test/support/docs.ex file for definitions of TeslaDocsTest.* modules

    test "generate docs by default" do
      {:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(TeslaDocsTest.Default)
      assert {_, _, _, doc, _} = Enum.find(docs, &match?({{:function, :get, 3}, _, _, _, _}, &1))
      assert doc != :hidden
    end

    test "do not generate docs for HTTP methods when docs: false" do
      {:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(TeslaDocsTest.NoDocs)

      assert {_, _, _, doc, _} = Enum.find(docs, &match?({{:function, :get, 3}, _, _, _, _}, &1))
      assert doc == :hidden

      assert {_, _, _, doc, _} = Enum.find(docs, &match?({{:function, :custom, 1}, _, _, _, _}, &1))
      assert doc["en"] =~ ~r/something/
    end
  end
end


modal accepts only socket

modal accepts only socket

defmodule Drab.Modal do
  require Logger
  import Drab.Template

  @moduledoc """
  Drab Module to launch Bootstrap Modal in the browser.

  Requires Bootstrap to work. Because there are differences beetween Bootstrap 3 and 4, you should
  configure which version you use (by default it is `:bootstrap3`):

      config :drab, :modal_css, :bootstrap4
  """

  use DrabModule
  @impl true
  def js_templates(), do: ["drab.modal.js"]

  @doc """
  Modal, synchronous alert box. This function shows Bootstrap modal window on the browser and waits
  for the user input.

  Parameters:
  * title - title of the message box
  * body - html with the body of the alert box. When contains input, selects, etc, this function
  return their values

  Options:
  * class - additional classes to .modal-dialog, ex. modal-lg, modal-sm, modal-xs
  * buttons - list of name/text of the buttons (:ok, :cancel are only available names by default;
    you need to create a template if you want more buttons), eq. [ok: "Yes", cancel: "No"]
  * timeout - in milliseconds - after this time modal window will close and the function will
    return {:cancel, _}

  Returns a tuple {clicked_button, params}, where:
  * clicked_button is an atom of `:ok` or `:cancel`. Notice that pressing `esc` or closing
    the modal window will return `:cancel`, while pressing `enter` returns `:ok`
  * params: Map `%{name|id => value}` of all inputs, selects, etc which are in the alert box body.
    Uses `name` attribute as a key, or `id`, when there is no `name`. If there is no `id` or
    `name`, this form value will not be included to the output.

  Examples:

      socket |> alert("Title", "Shows this message with default OK button")

      # Yes/No requester, returns :ok or :cancel
      {button, _} = socket |> alert("Message", "Sure?", buttons: [ok: "Azaliż", cancel: "Ney"])

      # messagebox with two input boxes in body
      form = "<input name='first'><input id='second'>"
      name = case socket |> alert("What's your name?", form, buttons: [ok: "OK", cancel: "No"]) do
        { :ok, params } -> "\#{params["first"]} \#{params["second"]}"
        { :cancel, _ }  -> "anonymous"
      end

  Templates used to generate HTML for the alert box could be found in
  `deps/drab/priv/templates/drab/`. If you want to modify it, copy them to `priv/templates/drab`
  in your application. There are two templates for default `:ok` and `:cancel` buttons, but
  you may create new one and use them in the same way. For example, to have a new button called
  `unspecified` create a template `priv/templates/drab/modal.alert.button.unspecified.html.eex`:

      <button id="_drab_modal_button_unspecified" name="unspecified" type="button"
       class="btn btn-default drab-modal-button" data-dismiss="modal">
          <%= label %>
      </button>

  The button must have `drab-modal-button` class and its name should correspond to key in `buttons`
  list. Now you can use your button in the same way as `:ok` and `:cancel`

      {button, _} =
        socket |> alert("3 buttons", "Choice?",
                  buttons: [ok: "Yes", cancel: "No", unspecified: "Don't know"])

  """
  @spec alert(Phoenix.Socket.t(), String.t(), String.t(), Keyword.t()) ::
          Drab.Core.return() | no_return
  @spec alert(Phoenix.Socket.t(), String.t(), String.t()) :: Drab.Core.return() | no_return
  def alert(%Phoenix.Socket{} = socket, title, body, options \\ []) do
    buttons = options[:buttons] || [ok: "OK"]

    bindings = [
      title: title,
      body: body,
      class: options[:class],
      buttons: buttons_html(socket, buttons)
    ]

    html =
      render_template(
        socket.endpoint,
        "modal.alert.#{Drab.Config.get(:modal_css)}.html.eex",
        bindings
      )

    {:ok, result} =
      Drab.push_and_wait_forever(socket, self(), "modal", timeout: options[:timeout], html: html)

    result
  end

  @spec buttons_html(Phoenix.Socket.t(), Keyword.t()) :: String.t()
  defp buttons_html(socket, buttons) do
    buttons
    |> Enum.map(fn {button, label} ->
      render_template(
        socket.endpoint,
        "modal.alert.button.#{Atom.to_string(button)}.html.eex",
        label: label
      )
    end)
    |> Enum.join("\n")
  end
end

defmodule Drab.Modal do
  require Logger
  import Drab.Template

  @moduledoc """
  Drab Module to launch Bootstrap Modal in the browser.

  Requires Bootstrap to work. Because there are differences beetween Bootstrap 3 and 4, you should
  configure which version you use (by default it is `:bootstrap3`):

      config :drab, :modal_css, :bootstrap4
  """

  use DrabModule
  @impl true
  def js_templates(), do: ["drab.modal.js"]

  @doc """
  Modal, synchronous alert box. This function shows Bootstrap modal window on the browser and waits
  for the user input.

  Parameters:
  * title - title of the message box
  * body - html with the body of the alert box. When contains input, selects, etc, this function
  return their values

  Options:
  * class - additional classes to .modal-dialog, ex. modal-lg, modal-sm, modal-xs
  * buttons - list of name/text of the buttons (:ok, :cancel are only available names by default;
    you need to create a template if you want more buttons), eq. [ok: "Yes", cancel: "No"]
  * timeout - in milliseconds - after this time modal window will close and the function will
    return {:cancel, _}

  Returns a tuple {clicked_button, params}, where:
  * clicked_button is an atom of `:ok` or `:cancel`. Notice that pressing `esc` or closing
    the modal window will return `:cancel`, while pressing `enter` returns `:ok`
  * params: Map `%{name|id => value}` of all inputs, selects, etc which are in the alert box body.
    Uses `name` attribute as a key, or `id`, when there is no `name`. If there is no `id` or
    `name`, this form value will not be included to the output.

  Examples:

      socket |> alert("Title", "Shows this message with default OK button")

      # Yes/No requester, returns :ok or :cancel
      {button, _} = socket |> alert("Message", "Sure?", buttons: [ok: "Azaliż", cancel: "Ney"])

      # messagebox with two input boxes in body
      form = "<input name='first'><input id='second'>"
      name = case socket |> alert("What's your name?", form, buttons: [ok: "OK", cancel: "No"]) do
        { :ok, params } -> "\#{params["first"]} \#{params["second"]}"
        { :cancel, _ }  -> "anonymous"
      end

  Templates used to generate HTML for the alert box could be found in
  `deps/drab/priv/templates/drab/`. If you want to modify it, copy them to `priv/templates/drab`
  in your application. There are two templates for default `:ok` and `:cancel` buttons, but
  you may create new one and use them in the same way. For example, to have a new button called
  `unspecified` create a template `priv/templates/drab/modal.alert.button.unspecified.html.eex`:

      <button id="_drab_modal_button_unspecified" name="unspecified" type="button"
       class="btn btn-default drab-modal-button" data-dismiss="modal">
          <%= label %>
      </button>

  The button must have `drab-modal-button` class and its name should correspond to key in `buttons`
  list. Now you can use your button in the same way as `:ok` and `:cancel`

      {button, _} =
        socket |> alert("3 buttons", "Choice?",
                  buttons: [ok: "Yes", cancel: "No", unspecified: "Don't know"])

  """
  @spec alert(Phoenix.Socket.t(), String.t(), String.t(), Keyword.t()) ::
          Drab.Core.return() | no_return
  @spec alert(Phoenix.Socket.t(), String.t(), String.t()) :: Drab.Core.return() | no_return
  def alert(socket, title, body, options \\ []) do
    buttons = options[:buttons] || [ok: "OK"]

    bindings = [
      title: title,
      body: body,
      class: options[:class],
      buttons: buttons_html(socket, buttons)
    ]

    html =
      render_template(
        socket.endpoint,
        "modal.alert.#{Drab.Config.get(:modal_css)}.html.eex",
        bindings
      )

    {:ok, result} =
      Drab.push_and_wait_forever(socket, self(), "modal", timeout: options[:timeout], html: html)

    result
  end

  @spec buttons_html(Phoenix.Socket.t(), Keyword.t()) :: String.t()
  defp buttons_html(socket, buttons) do
    buttons
    |> Enum.map(fn {button, label} ->
      render_template(
        socket.endpoint,
        "modal.alert.button.#{Atom.to_string(button)}.html.eex",
        label: label
      )
    end)
    |> Enum.join("\n")
  end
end


Refactor create into smaller functions, use pipe when needed

defmodule Gittp.Repo do
    require Logger

    def full_path(repo, file_path) do
        [repo.path, file_path]
        |> Path.join
        |> Path.expand
    end

    def content(repo, path) do
        case Gittp.Cache.get_file_content(path) do
            nil -> case File.dir? full_path(repo, path) do
                false -> {:error, :enoent}
                true -> dir_content repo, path
            end
            file -> {:ok, file}
        end
    end

    def from_local(local_repo_path) do
        repo = Git.new local_repo_path
        Git.pull repo, ~w(--rebase upstream master)
        Logger.info "pulled latest changes from upstream"
        repo
    end

    def clone(remote_repo_url, local_repo_path) do
        Logger.info "cloning remote repo"
        {:ok, repo} = Git.clone [remote_repo_url, local_repo_path]
        Git.remote repo, ["add", "upstream", remote_repo_url]
        Logger.info "cloned " <> remote_repo_url
        repo
    end

    def write(repo, commit) do
        case checksum_valid?(repo, commit.checksum, commit.path) do
            false -> {:error, :checksum_mismatch}
            true -> write_and_push(repo, commit)
        end
    end

    def create(repo, commit) do
        absolute_path = full_path(repo, commit.path)
        case File.exists? absolute_path do
            false ->
                case absolute_path
                |> Path.dirname
                |> File.mkdir_p do
                    {:ok, _} ->
                        create_file_in_git(repo, commit, absolute_path)
                    error -> error
                end

            true -> {:error, :file_exists}
        end
    end

    defp create_file_in_git(repo, commit, absolute_path) do
        case File.open(absolute_path, [:write]) do
            {:ok, file} ->
                IO.write file, commit.content
                File.close file
                commit_and_push(repo, commit)
            error -> error
        end
    end

    defp dir_content(repo, path) do
        absolute_path = full_path(repo, path)
        case File.ls absolute_path do
            {:ok, content} -> {:ok, %{"content" => content,
                                       "path" => path,
                                       "isDirectory" => true}}
            error -> error
        end
    end

    defp file_content(repo, path) do
        absolute_path = full_path(repo, path)
        case File.read absolute_path do
            {:ok, content} -> {:ok, %{"content" => content,
                                      "checksum" => Gittp.Utils.hash_string(content),
                                      "path" => path,
                                      "isDirectory" => false}}
            error -> error
        end
    end

    defp write_and_push(repo, commit) do
        case File.write full_path(repo, commit.path), commit.content do
            :ok -> commit_and_push(repo, commit)
            error -> {:error, error}
        end
    end

    defp commit_and_push(repo, commit) do
        Git.add repo, "."
        Git.commit repo, ["-m", commit.commit_message, "--author", commit.author]
        case Git.pull repo do
            {:ok, _} -> case Git.push repo do
                            {:ok, message} -> {:ok, message}
                            {:error, message} ->
                                 Git.reset repo, ~w(--hard HEAD~1)
                                 {:ok, {:error, message}}
                        end
            {:error, message} ->
                 Git.reset repo, ~w(--hard HEAD~1)
                {:ok, message}
        end
    end

    defp checksum_valid?(repo, checksum, file_path) do
        full_path = full_path(repo, file_path)
        File.exists?(full_path) and Gittp.Utils.hash_file(full_path) == checksum
    end
end

defmodule Gittp.Repo do
    require Logger

    def full_path(repo, file_path) do
        [repo.path, file_path]
        |> Path.join
        |> Path.expand
    end

    def content(repo, path) do
        case Gittp.Cache.get_file_content(path) do
            nil -> case File.dir? full_path(repo, path) do
                false -> {:error, :enoent}
                true -> dir_content repo, path
            end
            file -> {:ok, file}
        end
    end

    def from_local(local_repo_path) do
        repo = Git.new local_repo_path
        Git.pull repo, ~w(--rebase upstream master)
        Logger.info "pulled latest changes from upstream"
        repo
    end

    def clone(remote_repo_url, local_repo_path) do
        Logger.info "cloning remote repo"
        {:ok, repo} = Git.clone [remote_repo_url, local_repo_path]
        Git.remote repo, ["add", "upstream", remote_repo_url]
        Logger.info "cloned " <> remote_repo_url
        repo
    end

    def write(repo, commit) do
        case checksum_valid?(repo, commit.checksum, commit.path) do
            false -> {:error, :checksum_mismatch}
            true -> write_and_push(repo, commit)
        end
    end

    def create(repo, commit) do
        absolute_path = full_path(repo, commit.path)
        case File.exists? absolute_path do
            false ->
                case File.mkdir_p(Path.dirname(absolute_path)) do
                    {:ok, _} ->
                        case File.open(absolute_path, [:write]) do
                            {:ok, file} ->
                                IO.write file, commit.content
                                File.close file
                                commit_and_push(repo, commit)
                            error -> error
                        end
                    error -> error
                end

            true -> {:error, :file_exists}
        end
    end

    defp dir_content(repo, path) do
        absolute_path = full_path(repo, path)
        case File.ls absolute_path do
            {:ok, content} -> {:ok, %{"content" => content,
                                       "path" => path,
                                       "isDirectory" => true}}
            error -> error
        end
    end

    defp file_content(repo, path) do
        absolute_path = full_path(repo, path)
        case File.read absolute_path do
            {:ok, content} -> {:ok, %{"content" => content,
                                      "checksum" => Gittp.Utils.hash_string(content),
                                      "path" => path,
                                      "isDirectory" => false}}
            error -> error
        end
    end

    defp write_and_push(repo, commit) do
        Logger.info "write a file " <> full_path(repo, commit.path)
        case File.write full_path(repo, commit.path), commit.content do
            :ok -> commit_and_push(repo, commit)
            error -> {:error, error}
        end
    end

    defp commit_and_push(repo, commit) do
        Logger.info "commit and push"
        Git.add repo, "."
        Git.commit repo, ["-m", commit.commit_message, "--author", commit.author]
        case Git.pull repo do
            {:ok, _} -> case Git.push repo do
                            {:ok, message} -> {:ok, message}
                            {:error, message} ->
                                 Git.reset repo, ~w(--hard HEAD~1)
                                 {:ok, {:error, message}}
                        end
            {:error, message} ->
                 Git.reset repo, ~w(--hard HEAD~1)
                {:ok, message}
        end
    end

    defp checksum_valid?(repo, checksum, file_path) do
        full_path = full_path(repo, file_path)
        File.exists?(full_path) and Gittp.Utils.hash_file(full_path) == checksum
    end
end


Use lists correctly in Elixir

Use lists correctly in Elixir

You can't access them by position, because they're *not arrays*.

Fixes #55

defmodule Aelita2.Batcher do
  @moduledoc """
  The "Batcher" manages the backlog of batches that each project has.
  It implements this set of rules:

    * When a patch is reviewed ("r+'ed"),
      it gets added to the project's non-running batch.
      If no such batch exists, it creates it.
    * After a short delay, if there is no currently running batch,
      the project's non-running batch is started.
    * The project's CI is occasionally polled,
      if a batch is currently running.
      After polling, the completion logic is run.
    * If a notification related to the underlying CI is received,
      the completion logic is run.
    * When the completion logic is run, the batch is either
      bisected (if it failed and there are two or more patches in it),
      blocked (if it failed and there is only one patch in it),
      pushed to master (if it passed),
      or (if there are still CI jobs with no results) it is left alone.
  """

  use GenServer

  alias Aelita2.Repo
  alias Aelita2.Batch
  alias Aelita2.Batcher
  alias Aelita2.Patch
  alias Aelita2.Project
  alias Aelita2.Status
  alias Aelita2.LinkPatchBatch

  @poll_period 1000
  @github_api Application.get_env(:aelita2, Aelita2.GitHub)[:api]

  # Public API

  def start_link do
    GenServer.start_link(__MODULE__, :ok, name: Aelita2.Batcher)
  end

  def reviewed(patch_id) when is_integer(patch_id) do
    GenServer.cast(Aelita2.Batcher, {:reviewed, patch_id})
  end

  def status(commit, identifier, state, url) do
    GenServer.cast(Aelita2.Batcher, {:status, commit, identifier, state, url})
  end

  # Server callbacks

  def init(:ok) do
    Process.send_after(self(), :poll, @poll_period)
    {:ok, :ok}
  end

  def handle_cast(args, state) do
    Repo.transaction(fn -> do_handle_cast(args) end)
    {:noreply, state}
  end

  def do_handle_cast({:reviewed, patch_id}) do
    patch = Repo.get!(Patch.all(:awaiting_review), patch_id)
    batch = get_new_batch(patch.project_id)
    project_id = batch.project_id
    ^project_id = patch.project_id
    params = %{batch_id: batch.id, patch_id: patch.id}
    Repo.insert!(LinkPatchBatch.changeset(%LinkPatchBatch{}, params))
  end

  def do_handle_cast({:status, commit, identifier, state, url}) do
    batch = Repo.all(Batch.get_assoc_by_commit(commit))
    state = Status.numberize_state(state)
    case batch do
      [batch] ->
        batch.id
        |> Status.get_for_batch(identifier)
        |> Repo.update_all([set: [state: state, url: url]])
        if batch.state == Batch.numberize_state(:running) do
          maybe_complete_batch(batch)
        end
      [] -> :ok
    end
  end

  def handle_info(:poll, :ok) do
    Repo.transaction(&poll_all/0)
    Process.send_after(self(), :poll, @poll_period)
    {:noreply, :ok}
  end

  # Private implementation details

  defp poll_all do
    :incomplete
    |> Batch.all_assoc()
    |> Repo.all()
    |> Aelita2.Batcher.Queue.organize_batches_into_project_queues()
    |> Enum.each(&poll_batches/1)
  end

  defp poll_batches({:waiting, batches}) do
    case Enum.filter(batches, &Batch.next_poll_is_past/1) do
      [] -> :ok
      [batch | _] -> start_waiting_batch(batch)
    end
  end

  defp poll_batches({:running, batches}) do
    batch = hd(batches)
    if Batch.next_poll_is_past(batch) do
      poll_running_batch(batch)
    end
  end

  defp start_waiting_batch(batch) do
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()
    project = batch.project
    stmp = "#{project.staging_branch}.tmp"
    repo_conn = get_repo_conn(project)
    base = @github_api.copy_branch!(
      repo_conn,
      project.master_branch,
      stmp)
    do_merge_patch = fn patch, branch ->
      case branch do
        :conflict -> :conflict
        _ -> @github_api.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "-bors-staging-tmp-#{patch.pr_xref}"
          })
      end
    end
    head = with(
      %{tree: tree} <- Enum.reduce(patches, base, do_merge_patch),
      parents <- [base | Enum.map(patches, &(&1.commit))],
      commit_message <- Batcher.Message.generate_commit_message(patches),
      do: @github_api.synthesize_commit!(
        repo_conn,
        %{
          branch: project.staging_branch,
          tree: tree,
          parents: parents,
          commit_message: commit_message}))
    case head do
      :conflict ->
        state = bisect(patches, project)
        send_message(repo_conn, patches, {:conflict, state})
      commit ->
        state = setup_statuses(repo_conn, batch, patches)
        state = Batch.numberize_state(state)
        now = DateTime.to_unix(DateTime.utc_now(), :seconds)
        batch
        |> Batch.changeset(%{state: state, commit: commit, last_polled: now})
        |> Repo.update!()
    end
    Project.ping!(project.id)
  end

  defp setup_statuses(repo_conn, batch, patches) do
    toml = @github_api.get_file(
      repo_conn,
      batch.project.staging_branch,
      "bors.toml")
    case toml do
      nil ->
        setup_statuses_error(
          repo_conn,
          batch,
          patches,
          "bors.toml does not exist")
        :err
      toml ->
        case Aelita2.Batcher.BorsToml.new(toml) do
          {:ok, toml} ->
            toml.status
            |> Enum.map(&%Status{
                batch_id: batch.id,
                identifier: &1,
                url: nil,
                state: Status.numberize_state(:running)})
            |> Enum.each(&Repo.insert!/1)
            :running
          {:err, :parse_failed} ->
            setup_statuses_error(repo_conn,
              batch,
              patches,
              "bors.toml is invalid")
            :err
        end
    end
  end

  defp setup_statuses_error(repo_conn, batch, patches, message) do
    err = Batch.numberize_state(:err)
    batch
    |> Batch.changeset(%{state: err})
    |> Repo.update!()
    send_message(repo_conn, patches, {:config, message})
  end

  defp poll_running_batch(batch) do
    project = batch.project
    gh_statuses = project
    |> get_repo_conn()
    |> @github_api.get_commit_status!(batch.commit)
    |> Enum.map(&{elem(&1, 0), Status.numberize_state(elem(&1, 1))})
    |> Map.new()
    batch.id
    |> Status.all_for_batch()
    |> Repo.all()
    |> Enum.filter(&Map.has_key?(gh_statuses, &1.identifier))
    |> Enum.map(&{&1, %{state: Map.fetch!(gh_statuses, &1.identifier)}})
    |> Enum.map(&Status.changeset(elem(&1, 0), elem(&1, 1)))
    |> Enum.each(&Repo.update!/1)
    maybe_complete_batch(batch)
  end

  defp maybe_complete_batch(batch) do
    statuses = Repo.all(Status.all_for_batch(batch.id))
    state = Aelita2.Batcher.State.summary_statuses(statuses)
    maybe_complete_batch(state, batch, statuses)
    state = Batch.numberize_state(state)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    batch
    |> Batch.changeset(%{state: state, last_polled: now})
    |> Repo.update!()
  end

  defp maybe_complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    @github_api.push!(
      repo_conn,
      batch.commit,
      project.master_branch)
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()
    send_message(repo_conn, patches, {:succeeded, statuses})
    Project.ping!(project.id)
  end

  defp maybe_complete_batch(:err, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    erred = Enum.filter(statuses, &(&1.state == :err))
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()
    state = bisect(patches, project)
    send_message(repo_conn, patches, {state, erred})
    Project.ping!(project.id)
  end

  defp maybe_complete_batch(:running, _batch, _erred) do
    :ok
  end

  defp bisect(patches, project) do
    count = Enum.count(patches)
    if count > 1 do
      {patches_lo, patches_hi} = Enum.split(patches, div(count, 2))
      make_batch(patches_lo, project.id)
      make_batch(patches_hi, project.id)
      :retrying
    else
      :failed
    end
  end

  defp make_batch(patches, project_id) do
    batch = Repo.insert!(Batch.new(project_id))
    patches
    |> Enum.map(&%{batch_id: batch.id, patch_id: &1.id})
    |> Enum.map(&LinkPatchBatch.changeset(%LinkPatchBatch{}, &1))
    |> Enum.each(&Repo.insert!/1)
    batch
  end

  def get_new_batch(project_id) do
    waiting = Batch.numberize_state(:waiting)
    case Repo.get_by(Batch, project_id: project_id, state: waiting) do
      nil -> Repo.insert!(Batch.new(project_id))
      batch -> batch
    end
  end

  defp send_message(repo_conn, patches, message) do
    body = Batcher.Message.generate_message(message)
    Enum.each(patches, &@github_api.post_comment!(
      repo_conn,
      &1.pr_xref,
      body))
  end

  defp get_repo_conn(project) do
    project.repo_xref
    |> Project.installation_connection()
    |> Repo.one!()
    |> @github_api.RepoConnection.connect!()
  end
end

defmodule Aelita2.Batcher do
  @moduledoc """
  The "Batcher" manages the backlog of batches that each project has.
  It implements this set of rules:

    * When a patch is reviewed ("r+'ed"),
      it gets added to the project's non-running batch.
      If no such batch exists, it creates it.
    * After a short delay, if there is no currently running batch,
      the project's non-running batch is started.
    * The project's CI is occasionally polled,
      if a batch is currently running.
      After polling, the completion logic is run.
    * If a notification related to the underlying CI is received,
      the completion logic is run.
    * When the completion logic is run, the batch is either
      bisected (if it failed and there are two or more patches in it),
      blocked (if it failed and there is only one patch in it),
      pushed to master (if it passed),
      or (if there are still CI jobs with no results) it is left alone.
  """

  use GenServer

  alias Aelita2.Repo
  alias Aelita2.Batch
  alias Aelita2.Batcher
  alias Aelita2.Patch
  alias Aelita2.Project
  alias Aelita2.Status
  alias Aelita2.LinkPatchBatch

  @poll_period 1000
  @github_api Application.get_env(:aelita2, Aelita2.GitHub)[:api]

  # Public API

  def start_link do
    GenServer.start_link(__MODULE__, :ok, name: Aelita2.Batcher)
  end

  def reviewed(patch_id) when is_integer(patch_id) do
    GenServer.cast(Aelita2.Batcher, {:reviewed, patch_id})
  end

  def status(commit, identifier, state, url) do
    GenServer.cast(Aelita2.Batcher, {:status, commit, identifier, state, url})
  end

  # Server callbacks

  def init(:ok) do
    Process.send_after(self(), :poll, @poll_period)
    {:ok, :ok}
  end

  def handle_cast(args, state) do
    Repo.transaction(fn -> do_handle_cast(args) end)
    {:noreply, state}
  end

  def do_handle_cast({:reviewed, patch_id}) do
    patch = Repo.get!(Patch.all(:awaiting_review), patch_id)
    batch = get_new_batch(patch.project_id)
    project_id = batch.project_id
    ^project_id = patch.project_id
    params = %{batch_id: batch.id, patch_id: patch.id}
    Repo.insert!(LinkPatchBatch.changeset(%LinkPatchBatch{}, params))
  end

  def do_handle_cast({:status, commit, identifier, state, url}) do
    batch = Repo.all(Batch.get_assoc_by_commit(commit))
    state = Status.numberize_state(state)
    case batch do
      [batch] ->
        batch.id
        |> Status.get_for_batch(identifier)
        |> Repo.update_all([set: [state: state, url: url]])
        if batch.state == Batch.numberize_state(:running) do
          maybe_complete_batch(batch)
        end
      [] -> :ok
    end
  end

  def handle_info(:poll, :ok) do
    Repo.transaction(&poll_all/0)
    Process.send_after(self(), :poll, @poll_period)
    {:noreply, :ok}
  end

  # Private implementation details

  defp poll_all do
    :incomplete
    |> Batch.all_assoc()
    |> Repo.all()
    |> Aelita2.Batcher.Queue.organize_batches_into_project_queues()
    |> Enum.each(&poll_batches/1)
  end

  defp poll_batches({:waiting, batches}) do
    case Enum.filter(batches, &Batch.next_poll_is_past/1) do
      [] -> :ok
      [batch | _] -> start_waiting_batch(batch)
    end
  end

  defp poll_batches({:running, batches}) do
    batch = hd(batches)
    if Batch.next_poll_is_past(batch) do
      poll_running_batch(batch)
    end
  end

  defp start_waiting_batch(batch) do
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()
    project = batch.project
    stmp = "#{project.staging_branch}.tmp"
    repo_conn = get_repo_conn(project)
    base = @github_api.copy_branch!(
      repo_conn,
      project.master_branch,
      stmp)
    do_merge_patch = fn patch, branch ->
      case branch do
        :conflict -> :conflict
        _ -> @github_api.merge_branch!(
          repo_conn,
          %{
            from: patch.commit,
            to: stmp,
            commit_message: "-bors-staging-tmp-#{patch.pr_xref}"
          })
      end
    end
    head = with(
      %{tree: tree} <- Enum.reduce(patches, base, do_merge_patch),
      parents <- [base | Enum.map(patches, &(&1.commit))],
      commit_message <- Batcher.Message.generate_commit_message(patches),
      do: @github_api.synthesize_commit!(
        repo_conn,
        %{
          branch: project.staging_branch,
          tree: tree,
          parents: parents,
          commit_message: commit_message}))
    case head do
      :conflict ->
        state = bisect(patches)
        send_message(repo_conn, patches, {:conflict, state})
      commit ->
        state = setup_statuses(repo_conn, batch, patches)
        state = Batch.numberize_state(state)
        now = DateTime.to_unix(DateTime.utc_now(), :seconds)
        batch
        |> Batch.changeset(%{state: state, commit: commit, last_polled: now})
        |> Repo.update!()
    end
    Project.ping!(project.id)
  end

  defp setup_statuses(repo_conn, batch, patches) do
    toml = @github_api.get_file(
      repo_conn,
      batch.project.staging_branch,
      "bors.toml")
    case toml do
      nil ->
        setup_statuses_error(
          repo_conn,
          batch,
          patches,
          "bors.toml does not exist")
        :err
      toml ->
        case Aelita2.Batcher.BorsToml.new(toml) do
          {:ok, toml} ->
            toml.status
            |> Enum.map(&%Status{
                batch_id: batch.id,
                identifier: &1,
                url: nil,
                state: Status.numberize_state(:running)})
            |> Enum.each(&Repo.insert!/1)
            :running
          {:err, :parse_failed} ->
            setup_statuses_error(repo_conn,
              batch,
              patches,
              "bors.toml is invalid")
            :err
        end
    end
  end

  defp setup_statuses_error(repo_conn, batch, patches, message) do
    err = Batch.numberize_state(:err)
    batch
    |> Batch.changeset(%{state: err})
    |> Repo.update!()
    send_message(repo_conn, patches, {:config, message})
  end

  defp poll_running_batch(batch) do
    project = batch.project
    gh_statuses = project
    |> get_repo_conn()
    |> @github_api.get_commit_status!(batch.commit)
    |> Enum.map(&{elem(&1, 0), Status.numberize_state(elem(&1, 1))})
    |> Map.new()
    batch.id
    |> Status.all_for_batch()
    |> Repo.all()
    |> Enum.filter(&Map.has_key?(gh_statuses, &1.identifier))
    |> Enum.map(&{&1, %{state: Map.fetch!(gh_statuses, &1.identifier)}})
    |> Enum.map(&Status.changeset(elem(&1, 0), elem(&1, 1)))
    |> Enum.each(&Repo.update!/1)
    maybe_complete_batch(batch)
  end

  defp maybe_complete_batch(batch) do
    statuses = Repo.all(Status.all_for_batch(batch.id))
    state = Aelita2.Batcher.State.summary_statuses(statuses)
    maybe_complete_batch(state, batch, statuses)
    state = Batch.numberize_state(state)
    now = DateTime.to_unix(DateTime.utc_now(), :seconds)
    batch
    |> Batch.changeset(%{state: state, last_polled: now})
    |> Repo.update!()
  end

  defp maybe_complete_batch(:ok, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    @github_api.push!(
      repo_conn,
      batch.commit,
      project.master_branch)
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()
    send_message(repo_conn, patches, {:succeeded, statuses})
    Project.ping!(project.id)
  end

  defp maybe_complete_batch(:err, batch, statuses) do
    project = batch.project
    repo_conn = get_repo_conn(project)
    erred = Enum.filter(statuses, &(&1.state == :err))
    patches = batch.id
    |> Patch.all_for_batch()
    |> Repo.all()
    state = bisect(patches)
    send_message(repo_conn, patches, {state, erred})
    Project.ping!(project.id)
  end

  defp maybe_complete_batch(:running, _batch, _erred) do
    :ok
  end

  defp bisect(patches) do
    count = Enum.count(patches)
    if count > 1 do
      project = patches[0].project
      {patches_lo, patches_hi} = Enum.split(patches, div(count, 2))
      make_batch(patches_lo, project.id)
      make_batch(patches_hi, project.id)
      :retrying
    else
      :failed
    end
  end

  defp make_batch(patches, project_id) do
    batch = Repo.insert!(Batch.new(project_id))
    patches
    |> Enum.map(&%{batch_id: batch.id, patch_id: &1.id})
    |> Enum.map(&LinkPatchBatch.changeset(%LinkPatchBatch{}, &1))
    |> Enum.each(&Repo.insert!/1)
    batch
  end

  def get_new_batch(project_id) do
    waiting = Batch.numberize_state(:waiting)
    case Repo.get_by(Batch, project_id: project_id, state: waiting) do
      nil -> Repo.insert!(Batch.new(project_id))
      batch -> batch
    end
  end

  defp send_message(repo_conn, patches, message) do
    body = Batcher.Message.generate_message(message)
    Enum.each(patches, &@github_api.post_comment!(
      repo_conn,
      &1.pr_xref,
      body))
  end

  defp get_repo_conn(project) do
    project.repo_xref
    |> Project.installation_connection()
    |> Repo.one!()
    |> @github_api.RepoConnection.connect!()
  end
end