Initial commit (#124)

Initial commit (#124)

Don't write empty request map

defmodule Gateway.Plugins.Logger do
  @moduledoc """
  This plugin stores reqests and responses in `Logger` database.
  It is enabled by default and can not be disabled without rebuilding Annon container.

  All stored records can be accessible via [management API](http://docs.annon.apiary.io/#reference/requests).
  """
  use Gateway.Helpers.Plugin,
    plugin_name: "logger"

  alias Plug.Conn
  alias Gateway.DB.Schemas.Log
  alias Gateway.DB.Schemas.API, as: APISchema

  # TODO: Make one insert per HTTP request
  @doc false
  def call(conn, _opts) do
    conn
    |> log_request()
    |> Conn.register_before_send(fn conn ->
      conn
      |> log_response()
    end)
  end

  defp log_request(conn) do
    %{
      id: get_request_id(conn),
      idempotency_key: get_idempotency_key(conn) || "",
      ip_address: conn.remote_ip |> Tuple.to_list |> Enum.join("."),
      request: get_request_data(conn)
    }
    |> Log.create_request

    conn
  end

  defp log_response(conn) do
    conn
    |> get_request_id()
    |> Log.put_response(%{
      api: get_api_data(conn),
      consumer: get_consumer_data(conn),
      response: get_response_data(conn),
      latencies: get_latencies_data(conn),
      status_code: conn.status
    })

    conn
  end

  defp get_request_id(conn) do
    conn
    |> Conn.get_resp_header("x-request-id")
    |> Enum.at(0)
  end

  defp get_idempotency_key(conn) do
    conn
    |> Conn.get_req_header("x-idempotency-key")
    |> Enum.at(0)
  end

  defp modify_headers_list([]), do: []
  defp modify_headers_list([{key, value}|t]), do: [%{key => value}] ++ modify_headers_list(t)

  defp get_api_data(%Plug.Conn{private: %{api_config: nil}}), do: nil
  defp get_api_data(%Plug.Conn{private: %{api_config: %APISchema{id: id, name: name, request: request}}}) do
    %{
      id: id,
      name: name,
      request: prepare_params(request)
    }
  end

  defp get_consumer_data(_conn), do: %{} |> prepare_params

  defp get_request_data(conn) do
    %{
      method: conn.method,
      uri: conn.request_path,
      query: Plug.Conn.Query.decode(conn.query_string),
      headers: modify_headers_list(conn.req_headers),
      body: conn.body_params
    }
    |> prepare_params
  end

  defp get_response_data(conn) do
    %{
      status_code: conn.status,
      headers: modify_headers_list(conn.resp_headers),
      body: conn.resp_body
    }
    |> prepare_params
  end

  defp get_latencies_data(conn) do
    %{
      gateway: conn.assigns.latencies_gateway,
      upstream: "",
      client_request: ""
    }
    |> prepare_params
  end

  defp prepare_params(nil), do: %{}
  defp prepare_params(%{__struct__: _} = params), do: params |> Map.delete(:__struct__) |> prepare_params()
  defp prepare_params(params), do: for {key, val} <- params, into: %{}, do: {key_to_atom(key), val}

  defp key_to_atom(key) when is_binary(key), do: String.to_atom(key)
  defp key_to_atom(key) when is_atom(key), do: key
end

defmodule Gateway.Plugins.Logger do
  @moduledoc """
  This plugin stores reqests and responses in `Logger` database.
  It is enabled by default and can not be disabled without rebuilding Annon container.

  All stored records can be accessible via [management API](http://docs.annon.apiary.io/#reference/requests).
  """
  use Gateway.Helpers.Plugin,
    plugin_name: "logger"

  alias Plug.Conn
  alias Gateway.DB.Schemas.Log
  alias Gateway.DB.Schemas.API, as: APISchema

  # TODO: Make one insert per HTTP request
  @doc false
  def call(conn, _opts) do
    conn
    |> log_request()
    |> Conn.register_before_send(fn conn ->
      conn
      |> log_response()
    end)
  end

  defp log_request(conn) do
    %{
      id: get_request_id(conn),
      idempotency_key: get_idempotency_key(conn) || "",
      ip_address: conn.remote_ip |> Tuple.to_list |> Enum.join("."),
      request: get_request_data(conn)
    }
    |> Log.create_request

    conn
  end

  defp log_response(conn) do
    conn
    |> get_request_id()
    |> Log.put_response(%{
      api: get_api_data(conn),
      consumer: get_consumer_data(conn),
      response: get_response_data(conn),
      latencies: get_latencies_data(conn),
      status_code: conn.status
    })

    conn
  end

  defp get_request_id(conn) do
    conn
    |> Conn.get_resp_header("x-request-id")
    |> Enum.at(0)
  end

  defp get_idempotency_key(conn) do
    conn
    |> Conn.get_req_header("x-idempotency-key")
    |> Enum.at(0)
  end

  defp modify_headers_list([]), do: []
  defp modify_headers_list([{key, value}|t]), do: [%{key => value}] ++ modify_headers_list(t)

  defp get_api_data(%Plug.Conn{private: %{api_config: nil}}), do: %{}
  defp get_api_data(%Plug.Conn{private: %{api_config: %APISchema{id: id, name: name, request: request}}}) do
    %{
      id: id,
      name: name,
      request: prepare_params(request)
    }
  end

  defp get_consumer_data(_conn), do: %{} |> prepare_params

  defp get_request_data(conn) do
    %{
      method: conn.method,
      uri: conn.request_path,
      query: Plug.Conn.Query.decode(conn.query_string),
      headers: modify_headers_list(conn.req_headers),
      body: conn.body_params
    }
    |> prepare_params
  end

  defp get_response_data(conn) do
    %{
      status_code: conn.status,
      headers: modify_headers_list(conn.resp_headers),
      body: conn.resp_body
    }
    |> prepare_params
  end

  defp get_latencies_data(conn) do
    %{
      gateway: conn.assigns.latencies_gateway,
      upstream: "",
      client_request: ""
    }
    |> prepare_params
  end

  defp prepare_params(nil), do: %{}
  defp prepare_params(%{__struct__: _} = params), do: params |> Map.delete(:__struct__) |> prepare_params()
  defp prepare_params(params), do: for {key, val} <- params, into: %{}, do: {key_to_atom(key), val}

  defp key_to_atom(key) when is_binary(key), do: String.to_atom(key)
  defp key_to_atom(key) when is_atom(key), do: key
end


Remove unused alias

Remove unused alias

defmodule BattleSnake.WorldTest do
  alias BattleSnake.{World, Point, Snake}
  use ExUnit.Case, async: true
  use Property

  setup context do
    world = %World{
      max_food: 4,
      height: 10,
      width: 10,
    }
    Map.put context, :world, world
  end

  describe "#rand_unoccupied_space" do
    property "in the bounds of the board, on an unoccupied space" do
      forall world(), fn w ->
        point = World.rand_unoccupied_space(w)

        coords = Enum.flat_map w.snakes, &(&1.coords)

        ( not point in w.food and
          not point in coords and
          point.x in World.cols(w) and
          point.y in World.rows(w))
      end
    end
  end

  describe "#stock_food" do
    test "sets food on the board, up to a max", %{world: world} do
      world = World.stock_food(world)
      assert length(world.food) == 4
    end

    test "sets no food if set to 0", %{world: world} do
      world = put_in world.max_food, 0
      world = World.stock_food(world)
      assert length(world.food) == 0
    end

    test "adds to existing stocks", %{world: world} do
      food = %Point{y: 5, x: 5}
      world = put_in world.food, [food]
      world = put_in world.max_food, 2

      world = World.stock_food(world)

      assert([
        %Point{},
        ^food
      ] = world.food)
    end
  end

  describe "#clean_up_dead" do
    test "removes any snakes that die in head to heads", %{world: world} do
      snake = %Snake{coords: [%Point{y: 5, x: 5}]}

      world = put_in world.snakes, [snake, snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake, snake]
    end

    test "removes snakes that died in body collisions", %{world: world} do
      snake = %Snake{coords: [%Point{y: 5, x: 5}, %Point{y: 5, x: 5}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end

    test "removes any snakes that die this turn", %{world: world} do
      snake = %Snake{coords: [%Point{y: 10, x: 10}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end
  end

  describe "#tick" do
    test "eating food" do
      world = %World{
        width: 10,
        height: 10,
        max_food: 1,
        food: [
          %Point{x: 0, y: 0},
        ],
        snakes: [
          %Snake{
            name: "Snake",
            coords: [
              %Point{x: 0, y: 1},
              %Point{x: 1, y: 1},
            ]
          }
        ]
      }

      moves = %{"Snake" => "up"}

      world = World.tick(world, moves)

      assert([
        %Snake{
          name: "Snake",
          coords: [
            %Point{x: 0, y: 0},
            %Point{x: 0, y: 1},
            %Point{x: 0, y: 1},
          ]
        }
      ]
      == world.snakes)
    end
  end
end

defmodule BattleSnake.WorldTest do
  alias BattleSnake.{World, Point, Snake}
  alias BattleSnake.Point, as: P
  use ExUnit.Case, async: true
  use Property

  setup context do
    world = %World{
      max_food: 4,
      height: 10,
      width: 10,
    }
    Map.put context, :world, world
  end

  describe "#rand_unoccupied_space" do
    property "in the bounds of the board, on an unoccupied space" do
      forall world(), fn w ->
        point = World.rand_unoccupied_space(w)

        coords = Enum.flat_map w.snakes, &(&1.coords)

        ( not point in w.food and
          not point in coords and
          point.x in World.cols(w) and
          point.y in World.rows(w))
      end
    end
  end

  describe "#stock_food" do
    test "sets food on the board, up to a max", %{world: world} do
      world = World.stock_food(world)
      assert length(world.food) == 4
    end

    test "sets no food if set to 0", %{world: world} do
      world = put_in world.max_food, 0
      world = World.stock_food(world)
      assert length(world.food) == 0
    end

    test "adds to existing stocks", %{world: world} do
      food = %Point{y: 5, x: 5}
      world = put_in world.food, [food]
      world = put_in world.max_food, 2

      world = World.stock_food(world)

      assert([
        %Point{},
        ^food
      ] = world.food)
    end
  end

  describe "#clean_up_dead" do
    test "removes any snakes that die in head to heads", %{world: world} do
      snake = %Snake{coords: [%Point{y: 5, x: 5}]}

      world = put_in world.snakes, [snake, snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake, snake]
    end

    test "removes snakes that died in body collisions", %{world: world} do
      snake = %Snake{coords: [%Point{y: 5, x: 5}, %Point{y: 5, x: 5}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end

    test "removes any snakes that die this turn", %{world: world} do
      snake = %Snake{coords: [%Point{y: 10, x: 10}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end
  end

  describe "#tick" do
    test "eating food" do
      world = %World{
        width: 10,
        height: 10,
        max_food: 1,
        food: [
          %Point{x: 0, y: 0},
        ],
        snakes: [
          %Snake{
            name: "Snake",
            coords: [
              %Point{x: 0, y: 1},
              %Point{x: 1, y: 1},
            ]
          }
        ]
      }

      moves = %{"Snake" => "up"}

      world = World.tick(world, moves)

      assert([
        %Snake{
          name: "Snake",
          coords: [
            %Point{x: 0, y: 0},
            %Point{x: 0, y: 1},
            %Point{x: 0, y: 1},
          ]
        }
      ]
      == world.snakes)
    end
  end
end


Fix non-working example usage in phx.server doc (#2697)

The --no-deps-check flag has no effect when used with the phx.server
task, because Mix always runs the dependency check before loading
tasks from dependencies. This replaces the --no-deps-check example
to a working one using --no-compile, and suggests a workaround to
run phx.server without checking dependencies.

Fixes #2690.

defmodule Mix.Tasks.Phx.Server do
  use Mix.Task

  @shortdoc "Starts applications and their servers"

  @moduledoc """
  Starts the application by configuring all endpoints servers to run.

  ## Command line options

  This task accepts the same command-line arguments as `run`.
  For additional information, refer to the documentation for
  `Mix.Tasks.Run`.

  For example, to run `phx.server` without recompiling:

      mix phx.server --no-compile

  The `--no-halt` flag is automatically added.

  Note that the `--no-deps-check` flag cannot be used this way,
  because Mix needs to check dependencies to find `phx.server`.

  To run `phx.server` without checking dependencies, you can run:

      mix do deps.loadpaths --no-deps-check, phx.server
  """

  @doc false
  def run(args) do
    Application.put_env(:phoenix, :serve_endpoints, true, persistent: true)
    Mix.Tasks.Run.run run_args() ++ args
  end

  defp run_args do
    if iex_running?(), do: [], else: ["--no-halt"]
  end

  defp iex_running? do
    Code.ensure_loaded?(IEx) and IEx.started?
  end
end

defmodule Mix.Tasks.Phx.Server do
  use Mix.Task

  @shortdoc "Starts applications and their servers"

  @moduledoc """
  Starts the application by configuring all endpoints servers to run.

  ## Command line options

  This task accepts the same command-line arguments as `run`.
  For additional information, refer to the documentation for
  `Mix.Tasks.Run`.

  For example, to run `phx.server` without checking dependencies:

      mix phx.server --no-deps-check

  The `--no-halt` flag is automatically added.
  """

  @doc false
  def run(args) do
    Application.put_env(:phoenix, :serve_endpoints, true, persistent: true)
    Mix.Tasks.Run.run run_args() ++ args
  end

  defp run_args do
    if iex_running?(), do: [], else: ["--no-halt"]
  end

  defp iex_running? do
    Code.ensure_loaded?(IEx) and IEx.started?
  end
end


Update description of formatter in Mix.Tasks.Test moduledoc (#9224)

defmodule Mix.Tasks.Test do
  defmodule Cover do
    @default_threshold 90

    @moduledoc false

    def start(compile_path, opts) do
      Mix.shell().info("Cover compiling modules ...")
      _ = :cover.stop()
      _ = :cover.start()

      case :cover.compile_beam_directory(compile_path |> to_charlist) do
        results when is_list(results) ->
          :ok

        {:error, _} ->
          Mix.raise("Failed to cover compile directory: " <> compile_path)
      end

      fn ->
        Mix.shell().info("\nGenerating cover results ...\n")
        {:result, ok, _fail} = :cover.analyse(:coverage, :line)

        {module_results, totals} = gather_coverage(ok, :cover.modules())
        module_results = Enum.sort_by(module_results, &percentage(elem(&1, 1)), &>=/2)

        if summary_opts = Keyword.get(opts, :summary, true) do
          console(module_results, totals, summary_opts)
        end

        html(module_results, opts)
      end
    end

    defp gather_coverage(results, keep) do
      keep_set = MapSet.new(keep)

      # When gathering coverage results, we need to skip any
      # entry with line equal to 0 as those are generated code.
      #
      # We may also have multiple entries on the same line.
      # Each line is only considered once.
      #
      # We use ETS for performance, to avoid working with nested maps.
      table = :ets.new(__MODULE__, [:set, :private])

      try do
        for {{module, line}, cov} <- results, module in keep_set, line != 0 do
          case cov do
            {1, 0} -> :ets.insert(table, {{module, line}, true})
            {0, 1} -> :ets.insert_new(table, {{module, line}, false})
          end
        end

        module_results =
          for module <- keep,
              results = read_module_cover_results(table, module),
              do: {module, results}

        total_covered = :ets.select_count(table, [{{:_, true}, [], [true]}])
        total_not_covered = :ets.select_count(table, [{{:_, false}, [], [true]}])

        {module_results, {total_covered, total_not_covered}}
      after
        :ets.delete(table)
      end
    end

    defp read_module_cover_results(table, module) do
      covered = :ets.select_count(table, [{{{module, :_}, true}, [], [true]}])
      not_covered = :ets.select_count(table, [{{{module, :_}, false}, [], [true]}])
      {covered, not_covered}
    end

    defp console(results, totals, true), do: console(results, totals, [])

    defp console(results, totals, opts) when is_list(opts) do
      Mix.shell().info("Percentage | Module")
      Mix.shell().info("-----------|--------------------------")
      results |> Enum.sort() |> Enum.each(&display(&1, opts))
      Mix.shell().info("-----------|--------------------------")
      display({"Total", totals}, opts)
      Mix.shell().info("")
    end

    defp html(results, opts) do
      output = opts[:output]
      File.mkdir_p!(output)

      for {mod, _} <- results do
        {:ok, _} = :cover.analyse_to_file(mod, '#{output}/#{mod}.html', [:html])
      end

      Mix.shell().info([
        "Generated HTML coverage results in '",
        output,
        "' directory\n"
      ])
    end

    defp color(percentage, true), do: color(percentage, @default_threshold)
    defp color(_, false), do: ""
    defp color(percentage, threshold) when percentage > threshold, do: :green
    defp color(_, _), do: :red

    defp display({name, coverage}, opts) do
      threshold = Keyword.get(opts, :threshold, @default_threshold)
      percentage = percentage(coverage)

      Mix.shell().info([
        color(percentage, threshold),
        format(percentage, 9),
        "%",
        :reset,
        " | ",
        format_name(name)
      ])
    end

    defp percentage({0, 0}), do: 100.0
    defp percentage({covered, not_covered}), do: covered / (covered + not_covered) * 100

    defp format(number, length), do: :io_lib.format("~#{length}.2f", [number])

    defp format_name(name) when is_binary(name), do: name
    defp format_name(mod) when is_atom(mod), do: inspect(mod)
  end

  use Mix.Task

  alias Mix.Compilers.Test, as: CT

  @compile {:no_warn_undefined, [ExUnit, ExUnit.Filters]}
  @shortdoc "Runs a project's tests"
  @recursive true
  @preferred_cli_env :test

  @moduledoc """
  Runs the tests for a project.

  This task starts the current application, loads up
  `test/test_helper.exs` and then requires all files matching the
  `test/**/*_test.exs` pattern in parallel.

  A list of files can be given after the task name in order to select
  the files to compile:

      mix test test/some/particular/file_test.exs

  Tests in umbrella projects can be run from the root by specifying
  the full suite path, including `apps/my_app/test`, in which case
  recursive tests for other child apps will be skipped completely:

      # To run all tests for my_app from the umbrella root
      mix test apps/my_app/test

      # To run a given test file on my_app from the umbrella root
      mix test apps/my_app/test/some/particular/file_test.exs

  ## Command line options

    * `--color` - enables color in the output
    * `--cover` - runs coverage tool. See "Coverage" section below
    * `--exclude` - excludes tests that match the filter
    * `--failed` - runs only tests that failed the last time they ran
    * `--force` - forces compilation regardless of modification times
    * `--formatter` - sets the formatter module that will print the results.
      Defaults to ExUnit's built-in CLI formatter
    * `--include` - includes tests that match the filter
    * `--listen-on-stdin` - runs tests, and then listens on stdin. Receiving a newline will
      result in the tests being run again. Very useful when combined with `--stale` and
      external commands which produce output on stdout upon file system modifications
    * `--max-cases` - sets the maximum number of tests running asynchronously. Only tests from
      different modules run in parallel. Defaults to twice the number of cores
    * `--max-failures` - the suite stops evaluating tests when this number of test
      failures is reached. It runs all tests if omitted
    * `--no-archives-check` - does not check archives
    * `--no-color` - disables color in the output
    * `--no-compile` - does not compile, even if files require compilation
    * `--no-deps-check` - does not check dependencies
    * `--no-elixir-version-check` - does not check the Elixir version from `mix.exs`
    * `--no-start` - does not start applications after compilation
    * `--only` - runs only tests that match the filter
    * `--preload-modules` - preloads all modules defined in applications
    * `--raise` - raises if the test suite failed
    * `--seed` - seeds the random number generator used to randomize the order of tests;
      `--seed 0` disables randomization
    * `--slowest` - prints timing information for the N slowest tests.
      Automatically sets `--trace` and `--preload-modules`
    * `--stale` - runs only tests which reference modules that changed since the
      last time tests were ran with `--stale`. You can read more about this option
      in the "Stale" section below
    * `--timeout` - sets the timeout for the tests
    * `--trace` - runs tests with detailed reporting. Automatically sets `--max-cases` to `1`.
      Note that in trace mode test timeouts will be ignored as timeout is set to `:infinity`

  See `ExUnit.configure/1` for more information on configuration options.

  ## Filters

  ExUnit provides tags and filtering functionality that allow developers
  to select which tests to run. The most common functionality is to exclude
  some particular tests from running by default in your test helper file:

      # Exclude all external tests from running
      ExUnit.configure(exclude: [external: true])

  Then, whenever desired, those tests could be included in the run via the
  `--include` option:

      mix test --include external:true

  The example above will run all tests that have the external option set to
  `true`. It is also possible to include all examples that have a given tag,
  regardless of its value:

      mix test --include external

  Note that all tests are included by default, so unless they are excluded
  first (either in the test helper or via the `--exclude` option) the
  `--include` option has no effect.

  For this reason, Mix also provides an `--only` option that excludes all
  tests and includes only the given ones:

      mix test --only external

  Which is similar to:

      mix test --include external --exclude test

  It differs in that the test suite will fail if no tests are executed when the `--only` option is used.

  In case a single file is being tested, it is possible to pass one or more specific
  line numbers to run only those given tests:

      mix test test/some/particular/file_test.exs:12

  Which is equivalent to:

      mix test --exclude test --include line:12 test/some/particular/file_test.exs

  Or:

      mix test test/some/particular/file_test.exs:12:24

  Which is equivalent to:

      mix test --exclude test --include line:12 --include line:24 test/some/particular/file_test.exs

  If a given line starts a `describe` block, that line filter runs all tests in it.
  Otherwise, it runs the closest test on or before the given line number.

  ## Configuration

    * `:test_paths` - list of paths containing test files. Defaults to
      `["test"]` if the `test` directory exists; otherwise, it defaults to `[]`.
      It is expected that all test paths contain a `test_helper.exs` file

    * `:test_pattern` - a pattern to load test files. Defaults to `*_test.exs`

    * `:warn_test_pattern` - a pattern to match potentially misnamed test files
      and display a warning. Defaults to `*_test.ex`

    * `:test_coverage` - a set of options to be passed down to the coverage
      mechanism

  ## Coverage

  The `:test_coverage` configuration accepts the following options:

    * `:output` - the output directory for cover results. Defaults to `"cover"`
    * `:tool` - the coverage tool
    * `:summary` - summary output configuration; can be either a boolean
      or a keyword list. When a keyword list is passed, it can specify a `:threshold`,
      which is a boolean or numeric value that enables coloring of code coverage
      results in red or green depending on whether the percentage is below or
      above the specified threshold, respectively. Defaults to `[threshold: 90]`

  By default, a very simple wrapper around OTP's `cover` is used as a tool,
  but it can be overridden as follows:

      def project() do
        [
          ...
          test_coverage: [tool: CoverModule]
          ...
        ]
      end

  `CoverModule` can be any module that exports `start/2`, receiving the
  compilation path and the `test_coverage` options as arguments.
  It must return either `nil` or an anonymous function of zero arity that will
  be run after the test suite is done.

  ## "Stale"

  The `--stale` command line option attempts to run only those test files which
  reference modules that have changed since the last time you ran this task with
  `--stale`.

  The first time this task is run with `--stale`, all tests are run and a manifest
  is generated. On subsequent runs, a test file is marked "stale" if any modules it
  references (and any modules those modules reference, recursively) were modified
  since the last run with `--stale`. A test file is also marked "stale" if it has
  been changed since the last run with `--stale`.
  """

  @switches [
    force: :boolean,
    color: :boolean,
    cover: :boolean,
    trace: :boolean,
    max_cases: :integer,
    max_failures: :integer,
    include: :keep,
    exclude: :keep,
    seed: :integer,
    only: :keep,
    compile: :boolean,
    start: :boolean,
    timeout: :integer,
    raise: :boolean,
    deps_check: :boolean,
    archives_check: :boolean,
    elixir_version_check: :boolean,
    failed: :boolean,
    stale: :boolean,
    listen_on_stdin: :boolean,
    formatter: :keep,
    slowest: :integer,
    preload_modules: :boolean
  ]

  @cover [output: "cover", tool: Cover]

  @impl true
  def run(args) do
    {opts, files} = OptionParser.parse!(args, strict: @switches)

    if not Mix.Task.recursing?() do
      do_run(opts, args, files)
    else
      {files_in_apps_path, files_not_in_apps_path} =
        Enum.split_with(files, &String.starts_with?(&1, "apps/"))

      app = Mix.Project.config()[:app]
      current_app_path = "apps/#{app}/"

      files_in_current_app_path =
        for file <- files_in_apps_path,
            String.starts_with?(file, current_app_path) or not relative_app_file_exists?(file),
            do: String.trim_leading(file, current_app_path)

      files = files_in_current_app_path ++ files_not_in_apps_path

      if files == [] and files_in_apps_path != [] do
        :ok
      else
        do_run([test_location_relative_path: "apps/#{app}"] ++ opts, args, files)
      end
    end
  end

  defp relative_app_file_exists?(file) do
    {file, _} = ExUnit.Filters.parse_path(file)
    File.exists?(Path.join("../..", file))
  end

  defp do_run(opts, args, files) do
    if opts[:listen_on_stdin] do
      System.at_exit(fn _ ->
        IO.gets(:stdio, "")
        Mix.shell().info("Restarting...")
        :init.restart()
        Process.sleep(:infinity)
      end)
    end

    unless System.get_env("MIX_ENV") || Mix.env() == :test do
      Mix.raise("""
      "mix test" is running in the \"#{Mix.env()}\" environment. If you are \
      running tests from within another command, you can either:

        1. set MIX_ENV explicitly:

            MIX_ENV=test mix test.another

        2. set the :preferred_cli_env for a command inside "def project" in your mix.exs:

            preferred_cli_env: ["test.another": :test]
      """)
    end

    Mix.Task.run("loadpaths", args)

    if Keyword.get(opts, :compile, true) do
      Mix.Project.compile(args)
    end

    project = Mix.Project.config()

    # Start cover after we load deps but before we start the app.
    cover =
      if opts[:cover] do
        compile_path = Mix.Project.compile_path(project)
        cover = Keyword.merge(@cover, project[:test_coverage] || [])
        cover[:tool].start(compile_path, cover)
      end

    # Start the app and configure ExUnit with command line options
    # before requiring test_helper.exs so that the configuration is
    # available in test_helper.exs
    Mix.shell().print_app
    app_start_args = if opts[:slowest], do: ["--preload-modules" | args], else: args
    Mix.Task.run("app.start", app_start_args)

    # Ensure ExUnit is loaded.
    case Application.load(:ex_unit) do
      :ok -> :ok
      {:error, {:already_loaded, :ex_unit}} -> :ok
    end

    # Then configure ExUnit again so that command line options
    # override test_helper.exs
    {ex_unit_opts, allowed_files} = process_ex_unit_opts(opts)
    ExUnit.configure(ex_unit_opts)

    test_paths = project[:test_paths] || default_test_paths()
    Enum.each(test_paths, &require_test_helper(&1))
    ExUnit.configure(merge_helper_opts(ex_unit_opts))

    # Finally parse, require and load the files
    test_files = parse_files(files, test_paths)
    test_pattern = project[:test_pattern] || "*_test.exs"
    warn_test_pattern = project[:warn_test_pattern] || "*_test.ex"

    matched_test_files =
      test_files
      |> Mix.Utils.extract_files(test_pattern)
      |> filter_to_allowed_files(allowed_files)

    display_warn_test_pattern(test_files, test_pattern, matched_test_files, warn_test_pattern)

    case CT.require_and_run(matched_test_files, test_paths, opts) do
      {:ok, %{excluded: excluded, failures: failures, total: total}} ->
        cover && cover.()

        option_only_present? = Keyword.has_key?(opts, :only)

        cond do
          failures > 0 and opts[:raise] ->
            Mix.raise("\"mix test\" failed")

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

          excluded == total and option_only_present? ->
            message = "The --only option was given to \"mix test\" but no test was executed"
            raise_or_error_at_exit(message, opts)

          true ->
            :ok
        end

      :noop ->
        cond do
          opts[:stale] ->
            Mix.shell().info("No stale tests")

          files == [] ->
            Mix.shell().info("There are no tests to run")

          true ->
            message = "Paths given to \"mix test\" did not match any directory/file: "
            raise_or_error_at_exit(message <> Enum.join(files, ", "), opts)
        end

        :ok
    end
  end

  defp raise_or_error_at_exit(message, opts) do
    cond do
      opts[:raise] ->
        Mix.raise(message)

      Mix.Task.recursing?() ->
        Mix.shell().info(message)

      true ->
        Mix.shell().error(message)
        System.at_exit(fn _ -> exit({:shutdown, 1}) end)
    end
  end

  defp display_warn_test_pattern(test_files, test_pattern, matched_test_files, warn_test_pattern) do
    files = Mix.Utils.extract_files(test_files, warn_test_pattern) -- matched_test_files

    for file <- files do
      Mix.shell().info(
        "warning: #{file} does not match #{inspect(test_pattern)} and won't be loaded"
      )
    end
  end

  @option_keys [
    :trace,
    :max_cases,
    :max_failures,
    :include,
    :exclude,
    :seed,
    :timeout,
    :formatters,
    :colors,
    :slowest,
    :failures_manifest_file,
    :only_test_ids,
    :test_location_relative_path
  ]

  @doc false
  def process_ex_unit_opts(opts) do
    {opts, allowed_files} =
      opts
      |> manifest_opts()
      |> failed_opts()

    opts =
      opts
      |> filter_opts(:include)
      |> filter_opts(:exclude)
      |> filter_opts(:only)
      |> formatter_opts()
      |> color_opts()
      |> Keyword.take(@option_keys)
      |> default_opts()

    {opts, allowed_files}
  end

  defp merge_helper_opts(opts) do
    merge_opts(opts, :exclude)
  end

  defp default_opts(opts) do
    # Set autorun to false because Mix
    # automatically runs the test suite for us.
    [autorun: false] ++ opts
  end

  defp parse_files([], test_paths) do
    test_paths
  end

  defp parse_files([single_file], _test_paths) do
    # Check if the single file path matches test/path/to_test.exs:123. If it does,
    # apply "--only line:123" and trim the trailing :123 part.
    {single_file, opts} = ExUnit.Filters.parse_path(single_file)
    ExUnit.configure(opts)
    [single_file]
  end

  defp parse_files(files, _test_paths) do
    if Enum.any?(files, &match?({_, [_ | _]}, ExUnit.Filters.parse_path(&1))) do
      Mix.raise("Line numbers can only be used when running a single test file")
    else
      files
    end
  end

  defp parse_filters(opts, key) do
    if Keyword.has_key?(opts, key) do
      ExUnit.Filters.parse(Keyword.get_values(opts, key))
    end
  end

  defp filter_opts(opts, :only) do
    if filters = parse_filters(opts, :only) do
      opts
      |> Keyword.update(:include, filters, &(filters ++ &1))
      |> Keyword.update(:exclude, [:test], &[:test | &1])
    else
      opts
    end
  end

  defp filter_opts(opts, key) do
    if filters = parse_filters(opts, key) do
      Keyword.put(opts, key, filters)
    else
      opts
    end
  end

  defp formatter_opts(opts) do
    if Keyword.has_key?(opts, :formatter) do
      formatters =
        opts
        |> Keyword.get_values(:formatter)
        |> Enum.map(&Module.concat([&1]))

      Keyword.put(opts, :formatters, formatters)
    else
      opts
    end
  end

  @manifest_file_name ".mix_test_failures"

  defp manifest_opts(opts) do
    manifest_file = Path.join(Mix.Project.manifest_path(), @manifest_file_name)
    Keyword.put(opts, :failures_manifest_file, manifest_file)
  end

  defp failed_opts(opts) do
    if opts[:failed] do
      if opts[:stale] do
        Mix.raise("Combining --failed and --stale is not supported.")
      end

      {allowed_files, failed_ids} = ExUnit.Filters.failure_info(opts[:failures_manifest_file])
      {Keyword.put(opts, :only_test_ids, failed_ids), allowed_files}
    else
      {opts, nil}
    end
  end

  defp filter_to_allowed_files(matched_test_files, nil), do: matched_test_files

  defp filter_to_allowed_files(matched_test_files, %MapSet{} = allowed_files) do
    Enum.filter(matched_test_files, &MapSet.member?(allowed_files, Path.expand(&1)))
  end

  defp color_opts(opts) do
    case Keyword.fetch(opts, :color) do
      {:ok, enabled?} ->
        Keyword.put(opts, :colors, enabled: enabled?)

      :error ->
        opts
    end
  end

  defp merge_opts(opts, key) do
    value = List.wrap(Application.get_env(:ex_unit, key, []))
    Keyword.update(opts, key, value, &Enum.uniq(&1 ++ value))
  end

  defp require_test_helper(dir) do
    file = Path.join(dir, "test_helper.exs")

    if File.exists?(file) do
      Code.require_file(file)
    else
      Mix.raise("Cannot run tests because test helper file #{inspect(file)} does not exist")
    end
  end

  defp default_test_paths do
    if File.dir?("test") do
      ["test"]
    else
      []
    end
  end
end

defmodule Mix.Tasks.Test do
  defmodule Cover do
    @default_threshold 90

    @moduledoc false

    def start(compile_path, opts) do
      Mix.shell().info("Cover compiling modules ...")
      _ = :cover.stop()
      _ = :cover.start()

      case :cover.compile_beam_directory(compile_path |> to_charlist) do
        results when is_list(results) ->
          :ok

        {:error, _} ->
          Mix.raise("Failed to cover compile directory: " <> compile_path)
      end

      fn ->
        Mix.shell().info("\nGenerating cover results ...\n")
        {:result, ok, _fail} = :cover.analyse(:coverage, :line)

        {module_results, totals} = gather_coverage(ok, :cover.modules())
        module_results = Enum.sort_by(module_results, &percentage(elem(&1, 1)), &>=/2)

        if summary_opts = Keyword.get(opts, :summary, true) do
          console(module_results, totals, summary_opts)
        end

        html(module_results, opts)
      end
    end

    defp gather_coverage(results, keep) do
      keep_set = MapSet.new(keep)

      # When gathering coverage results, we need to skip any
      # entry with line equal to 0 as those are generated code.
      #
      # We may also have multiple entries on the same line.
      # Each line is only considered once.
      #
      # We use ETS for performance, to avoid working with nested maps.
      table = :ets.new(__MODULE__, [:set, :private])

      try do
        for {{module, line}, cov} <- results, module in keep_set, line != 0 do
          case cov do
            {1, 0} -> :ets.insert(table, {{module, line}, true})
            {0, 1} -> :ets.insert_new(table, {{module, line}, false})
          end
        end

        module_results =
          for module <- keep,
              results = read_module_cover_results(table, module),
              do: {module, results}

        total_covered = :ets.select_count(table, [{{:_, true}, [], [true]}])
        total_not_covered = :ets.select_count(table, [{{:_, false}, [], [true]}])

        {module_results, {total_covered, total_not_covered}}
      after
        :ets.delete(table)
      end
    end

    defp read_module_cover_results(table, module) do
      covered = :ets.select_count(table, [{{{module, :_}, true}, [], [true]}])
      not_covered = :ets.select_count(table, [{{{module, :_}, false}, [], [true]}])
      {covered, not_covered}
    end

    defp console(results, totals, true), do: console(results, totals, [])

    defp console(results, totals, opts) when is_list(opts) do
      Mix.shell().info("Percentage | Module")
      Mix.shell().info("-----------|--------------------------")
      results |> Enum.sort() |> Enum.each(&display(&1, opts))
      Mix.shell().info("-----------|--------------------------")
      display({"Total", totals}, opts)
      Mix.shell().info("")
    end

    defp html(results, opts) do
      output = opts[:output]
      File.mkdir_p!(output)

      for {mod, _} <- results do
        {:ok, _} = :cover.analyse_to_file(mod, '#{output}/#{mod}.html', [:html])
      end

      Mix.shell().info([
        "Generated HTML coverage results in '",
        output,
        "' directory\n"
      ])
    end

    defp color(percentage, true), do: color(percentage, @default_threshold)
    defp color(_, false), do: ""
    defp color(percentage, threshold) when percentage > threshold, do: :green
    defp color(_, _), do: :red

    defp display({name, coverage}, opts) do
      threshold = Keyword.get(opts, :threshold, @default_threshold)
      percentage = percentage(coverage)

      Mix.shell().info([
        color(percentage, threshold),
        format(percentage, 9),
        "%",
        :reset,
        " | ",
        format_name(name)
      ])
    end

    defp percentage({0, 0}), do: 100.0
    defp percentage({covered, not_covered}), do: covered / (covered + not_covered) * 100

    defp format(number, length), do: :io_lib.format("~#{length}.2f", [number])

    defp format_name(name) when is_binary(name), do: name
    defp format_name(mod) when is_atom(mod), do: inspect(mod)
  end

  use Mix.Task

  alias Mix.Compilers.Test, as: CT

  @compile {:no_warn_undefined, [ExUnit, ExUnit.Filters]}
  @shortdoc "Runs a project's tests"
  @recursive true
  @preferred_cli_env :test

  @moduledoc """
  Runs the tests for a project.

  This task starts the current application, loads up
  `test/test_helper.exs` and then requires all files matching the
  `test/**/*_test.exs` pattern in parallel.

  A list of files can be given after the task name in order to select
  the files to compile:

      mix test test/some/particular/file_test.exs

  Tests in umbrella projects can be run from the root by specifying
  the full suite path, including `apps/my_app/test`, in which case
  recursive tests for other child apps will be skipped completely:

      # To run all tests for my_app from the umbrella root
      mix test apps/my_app/test

      # To run a given test file on my_app from the umbrella root
      mix test apps/my_app/test/some/particular/file_test.exs

  ## Command line options

    * `--color` - enables color in the output
    * `--cover` - runs coverage tool. See "Coverage" section below
    * `--exclude` - excludes tests that match the filter
    * `--failed` - runs only tests that failed the last time they ran
    * `--force` - forces compilation regardless of modification times
    * `--formatter` - sets the formatter module that will print the results.
      Defaults to `ExUnit.CLIFormatter`
    * `--include` - includes tests that match the filter
    * `--listen-on-stdin` - runs tests, and then listens on stdin. Receiving a newline will
      result in the tests being run again. Very useful when combined with `--stale` and
      external commands which produce output on stdout upon file system modifications
    * `--max-cases` - sets the maximum number of tests running asynchronously. Only tests from
      different modules run in parallel. Defaults to twice the number of cores
    * `--max-failures` - the suite stops evaluating tests when this number of test
      failures is reached. It runs all tests if omitted
    * `--no-archives-check` - does not check archives
    * `--no-color` - disables color in the output
    * `--no-compile` - does not compile, even if files require compilation
    * `--no-deps-check` - does not check dependencies
    * `--no-elixir-version-check` - does not check the Elixir version from `mix.exs`
    * `--no-start` - does not start applications after compilation
    * `--only` - runs only tests that match the filter
    * `--preload-modules` - preloads all modules defined in applications
    * `--raise` - raises if the test suite failed
    * `--seed` - seeds the random number generator used to randomize the order of tests;
      `--seed 0` disables randomization
    * `--slowest` - prints timing information for the N slowest tests.
      Automatically sets `--trace` and `--preload-modules`
    * `--stale` - runs only tests which reference modules that changed since the
      last time tests were ran with `--stale`. You can read more about this option
      in the "Stale" section below
    * `--timeout` - sets the timeout for the tests
    * `--trace` - runs tests with detailed reporting. Automatically sets `--max-cases` to `1`.
      Note that in trace mode test timeouts will be ignored as timeout is set to `:infinity`

  See `ExUnit.configure/1` for more information on configuration options.

  ## Filters

  ExUnit provides tags and filtering functionality that allow developers
  to select which tests to run. The most common functionality is to exclude
  some particular tests from running by default in your test helper file:

      # Exclude all external tests from running
      ExUnit.configure(exclude: [external: true])

  Then, whenever desired, those tests could be included in the run via the
  `--include` option:

      mix test --include external:true

  The example above will run all tests that have the external option set to
  `true`. It is also possible to include all examples that have a given tag,
  regardless of its value:

      mix test --include external

  Note that all tests are included by default, so unless they are excluded
  first (either in the test helper or via the `--exclude` option) the
  `--include` option has no effect.

  For this reason, Mix also provides an `--only` option that excludes all
  tests and includes only the given ones:

      mix test --only external

  Which is similar to:

      mix test --include external --exclude test

  It differs in that the test suite will fail if no tests are executed when the `--only` option is used.

  In case a single file is being tested, it is possible to pass one or more specific
  line numbers to run only those given tests:

      mix test test/some/particular/file_test.exs:12

  Which is equivalent to:

      mix test --exclude test --include line:12 test/some/particular/file_test.exs

  Or:

      mix test test/some/particular/file_test.exs:12:24

  Which is equivalent to:

      mix test --exclude test --include line:12 --include line:24 test/some/particular/file_test.exs

  If a given line starts a `describe` block, that line filter runs all tests in it.
  Otherwise, it runs the closest test on or before the given line number.

  ## Configuration

    * `:test_paths` - list of paths containing test files. Defaults to
      `["test"]` if the `test` directory exists; otherwise, it defaults to `[]`.
      It is expected that all test paths contain a `test_helper.exs` file

    * `:test_pattern` - a pattern to load test files. Defaults to `*_test.exs`

    * `:warn_test_pattern` - a pattern to match potentially misnamed test files
      and display a warning. Defaults to `*_test.ex`

    * `:test_coverage` - a set of options to be passed down to the coverage
      mechanism

  ## Coverage

  The `:test_coverage` configuration accepts the following options:

    * `:output` - the output directory for cover results. Defaults to `"cover"`
    * `:tool` - the coverage tool
    * `:summary` - summary output configuration; can be either a boolean
      or a keyword list. When a keyword list is passed, it can specify a `:threshold`,
      which is a boolean or numeric value that enables coloring of code coverage
      results in red or green depending on whether the percentage is below or
      above the specified threshold, respectively. Defaults to `[threshold: 90]`

  By default, a very simple wrapper around OTP's `cover` is used as a tool,
  but it can be overridden as follows:

      def project() do
        [
          ...
          test_coverage: [tool: CoverModule]
          ...
        ]
      end

  `CoverModule` can be any module that exports `start/2`, receiving the
  compilation path and the `test_coverage` options as arguments.
  It must return either `nil` or an anonymous function of zero arity that will
  be run after the test suite is done.

  ## "Stale"

  The `--stale` command line option attempts to run only those test files which
  reference modules that have changed since the last time you ran this task with
  `--stale`.

  The first time this task is run with `--stale`, all tests are run and a manifest
  is generated. On subsequent runs, a test file is marked "stale" if any modules it
  references (and any modules those modules reference, recursively) were modified
  since the last run with `--stale`. A test file is also marked "stale" if it has
  been changed since the last run with `--stale`.
  """

  @switches [
    force: :boolean,
    color: :boolean,
    cover: :boolean,
    trace: :boolean,
    max_cases: :integer,
    max_failures: :integer,
    include: :keep,
    exclude: :keep,
    seed: :integer,
    only: :keep,
    compile: :boolean,
    start: :boolean,
    timeout: :integer,
    raise: :boolean,
    deps_check: :boolean,
    archives_check: :boolean,
    elixir_version_check: :boolean,
    failed: :boolean,
    stale: :boolean,
    listen_on_stdin: :boolean,
    formatter: :keep,
    slowest: :integer,
    preload_modules: :boolean
  ]

  @cover [output: "cover", tool: Cover]

  @impl true
  def run(args) do
    {opts, files} = OptionParser.parse!(args, strict: @switches)

    if not Mix.Task.recursing?() do
      do_run(opts, args, files)
    else
      {files_in_apps_path, files_not_in_apps_path} =
        Enum.split_with(files, &String.starts_with?(&1, "apps/"))

      app = Mix.Project.config()[:app]
      current_app_path = "apps/#{app}/"

      files_in_current_app_path =
        for file <- files_in_apps_path,
            String.starts_with?(file, current_app_path) or not relative_app_file_exists?(file),
            do: String.trim_leading(file, current_app_path)

      files = files_in_current_app_path ++ files_not_in_apps_path

      if files == [] and files_in_apps_path != [] do
        :ok
      else
        do_run([test_location_relative_path: "apps/#{app}"] ++ opts, args, files)
      end
    end
  end

  defp relative_app_file_exists?(file) do
    {file, _} = ExUnit.Filters.parse_path(file)
    File.exists?(Path.join("../..", file))
  end

  defp do_run(opts, args, files) do
    if opts[:listen_on_stdin] do
      System.at_exit(fn _ ->
        IO.gets(:stdio, "")
        Mix.shell().info("Restarting...")
        :init.restart()
        Process.sleep(:infinity)
      end)
    end

    unless System.get_env("MIX_ENV") || Mix.env() == :test do
      Mix.raise("""
      "mix test" is running in the \"#{Mix.env()}\" environment. If you are \
      running tests from within another command, you can either:

        1. set MIX_ENV explicitly:

            MIX_ENV=test mix test.another

        2. set the :preferred_cli_env for a command inside "def project" in your mix.exs:

            preferred_cli_env: ["test.another": :test]
      """)
    end

    Mix.Task.run("loadpaths", args)

    if Keyword.get(opts, :compile, true) do
      Mix.Project.compile(args)
    end

    project = Mix.Project.config()

    # Start cover after we load deps but before we start the app.
    cover =
      if opts[:cover] do
        compile_path = Mix.Project.compile_path(project)
        cover = Keyword.merge(@cover, project[:test_coverage] || [])
        cover[:tool].start(compile_path, cover)
      end

    # Start the app and configure ExUnit with command line options
    # before requiring test_helper.exs so that the configuration is
    # available in test_helper.exs
    Mix.shell().print_app
    app_start_args = if opts[:slowest], do: ["--preload-modules" | args], else: args
    Mix.Task.run("app.start", app_start_args)

    # Ensure ExUnit is loaded.
    case Application.load(:ex_unit) do
      :ok -> :ok
      {:error, {:already_loaded, :ex_unit}} -> :ok
    end

    # Then configure ExUnit again so that command line options
    # override test_helper.exs
    {ex_unit_opts, allowed_files} = process_ex_unit_opts(opts)
    ExUnit.configure(ex_unit_opts)

    test_paths = project[:test_paths] || default_test_paths()
    Enum.each(test_paths, &require_test_helper(&1))
    ExUnit.configure(merge_helper_opts(ex_unit_opts))

    # Finally parse, require and load the files
    test_files = parse_files(files, test_paths)
    test_pattern = project[:test_pattern] || "*_test.exs"
    warn_test_pattern = project[:warn_test_pattern] || "*_test.ex"

    matched_test_files =
      test_files
      |> Mix.Utils.extract_files(test_pattern)
      |> filter_to_allowed_files(allowed_files)

    display_warn_test_pattern(test_files, test_pattern, matched_test_files, warn_test_pattern)

    case CT.require_and_run(matched_test_files, test_paths, opts) do
      {:ok, %{excluded: excluded, failures: failures, total: total}} ->
        cover && cover.()

        option_only_present? = Keyword.has_key?(opts, :only)

        cond do
          failures > 0 and opts[:raise] ->
            Mix.raise("\"mix test\" failed")

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

          excluded == total and option_only_present? ->
            message = "The --only option was given to \"mix test\" but no test was executed"
            raise_or_error_at_exit(message, opts)

          true ->
            :ok
        end

      :noop ->
        cond do
          opts[:stale] ->
            Mix.shell().info("No stale tests")

          files == [] ->
            Mix.shell().info("There are no tests to run")

          true ->
            message = "Paths given to \"mix test\" did not match any directory/file: "
            raise_or_error_at_exit(message <> Enum.join(files, ", "), opts)
        end

        :ok
    end
  end

  defp raise_or_error_at_exit(message, opts) do
    cond do
      opts[:raise] ->
        Mix.raise(message)

      Mix.Task.recursing?() ->
        Mix.shell().info(message)

      true ->
        Mix.shell().error(message)
        System.at_exit(fn _ -> exit({:shutdown, 1}) end)
    end
  end

  defp display_warn_test_pattern(test_files, test_pattern, matched_test_files, warn_test_pattern) do
    files = Mix.Utils.extract_files(test_files, warn_test_pattern) -- matched_test_files

    for file <- files do
      Mix.shell().info(
        "warning: #{file} does not match #{inspect(test_pattern)} and won't be loaded"
      )
    end
  end

  @option_keys [
    :trace,
    :max_cases,
    :max_failures,
    :include,
    :exclude,
    :seed,
    :timeout,
    :formatters,
    :colors,
    :slowest,
    :failures_manifest_file,
    :only_test_ids,
    :test_location_relative_path
  ]

  @doc false
  def process_ex_unit_opts(opts) do
    {opts, allowed_files} =
      opts
      |> manifest_opts()
      |> failed_opts()

    opts =
      opts
      |> filter_opts(:include)
      |> filter_opts(:exclude)
      |> filter_opts(:only)
      |> formatter_opts()
      |> color_opts()
      |> Keyword.take(@option_keys)
      |> default_opts()

    {opts, allowed_files}
  end

  defp merge_helper_opts(opts) do
    merge_opts(opts, :exclude)
  end

  defp default_opts(opts) do
    # Set autorun to false because Mix
    # automatically runs the test suite for us.
    [autorun: false] ++ opts
  end

  defp parse_files([], test_paths) do
    test_paths
  end

  defp parse_files([single_file], _test_paths) do
    # Check if the single file path matches test/path/to_test.exs:123. If it does,
    # apply "--only line:123" and trim the trailing :123 part.
    {single_file, opts} = ExUnit.Filters.parse_path(single_file)
    ExUnit.configure(opts)
    [single_file]
  end

  defp parse_files(files, _test_paths) do
    if Enum.any?(files, &match?({_, [_ | _]}, ExUnit.Filters.parse_path(&1))) do
      Mix.raise("Line numbers can only be used when running a single test file")
    else
      files
    end
  end

  defp parse_filters(opts, key) do
    if Keyword.has_key?(opts, key) do
      ExUnit.Filters.parse(Keyword.get_values(opts, key))
    end
  end

  defp filter_opts(opts, :only) do
    if filters = parse_filters(opts, :only) do
      opts
      |> Keyword.update(:include, filters, &(filters ++ &1))
      |> Keyword.update(:exclude, [:test], &[:test | &1])
    else
      opts
    end
  end

  defp filter_opts(opts, key) do
    if filters = parse_filters(opts, key) do
      Keyword.put(opts, key, filters)
    else
      opts
    end
  end

  defp formatter_opts(opts) do
    if Keyword.has_key?(opts, :formatter) do
      formatters =
        opts
        |> Keyword.get_values(:formatter)
        |> Enum.map(&Module.concat([&1]))

      Keyword.put(opts, :formatters, formatters)
    else
      opts
    end
  end

  @manifest_file_name ".mix_test_failures"

  defp manifest_opts(opts) do
    manifest_file = Path.join(Mix.Project.manifest_path(), @manifest_file_name)
    Keyword.put(opts, :failures_manifest_file, manifest_file)
  end

  defp failed_opts(opts) do
    if opts[:failed] do
      if opts[:stale] do
        Mix.raise("Combining --failed and --stale is not supported.")
      end

      {allowed_files, failed_ids} = ExUnit.Filters.failure_info(opts[:failures_manifest_file])
      {Keyword.put(opts, :only_test_ids, failed_ids), allowed_files}
    else
      {opts, nil}
    end
  end

  defp filter_to_allowed_files(matched_test_files, nil), do: matched_test_files

  defp filter_to_allowed_files(matched_test_files, %MapSet{} = allowed_files) do
    Enum.filter(matched_test_files, &MapSet.member?(allowed_files, Path.expand(&1)))
  end

  defp color_opts(opts) do
    case Keyword.fetch(opts, :color) do
      {:ok, enabled?} ->
        Keyword.put(opts, :colors, enabled: enabled?)

      :error ->
        opts
    end
  end

  defp merge_opts(opts, key) do
    value = List.wrap(Application.get_env(:ex_unit, key, []))
    Keyword.update(opts, key, value, &Enum.uniq(&1 ++ value))
  end

  defp require_test_helper(dir) do
    file = Path.join(dir, "test_helper.exs")

    if File.exists?(file) do
      Code.require_file(file)
    else
      Mix.raise("Cannot run tests because test helper file #{inspect(file)} does not exist")
    end
  end

  defp default_test_paths do
    if File.dir?("test") do
      ["test"]
    else
      []
    end
  end
end


Fix small typo in Xref task docs (#10476)

defmodule Mix.Tasks.Xref do
  use Mix.Task

  import Mix.Compilers.Elixir,
    only: [read_manifest: 1, source: 0, source: 1, source: 2, module: 1]

  @shortdoc "Prints cross reference information"
  @recursive true
  @manifest "compile.elixir"

  @moduledoc """
  Prints cross reference information between modules.

  This task is automatically reenabled, so you can print information
  multiple times in the same Mix invocation.

  ## Xref modes

  The `xref` task expects a mode as first argument:

      mix xref MODE

  All available modes are discussed below.

  ### callers CALLEE

  Prints all callers of the given `MODULE`. Example:

      mix xref callers MyMod

  ### graph

  Prints a file dependency graph where an edge from `A` to `B` indicates
  that `A` (source) depends on `B` (sink).

      mix xref graph --format stats

  The following options are accepted:

    * `--exclude` - paths to exclude

    * `--label` - only shows relationships with the given label.
      By default, it keeps all labels that are transitive.
      The labels are "compile", "export" and "runtime". See
      "Dependencies types" section below

    * `--only-nodes` - only shows the node names (no edges)

    * `--only-direct` - the `--label` option will restrict itself
      to only direct dependencies instead of transitive ones

    * `--source` - displays all files that the given source file
      references (directly or indirectly)

    * `--sink` - displays all files that reference the given file
      (directly or indirectly)

    * `--min-cycle-size` - controls the minimum cycle size on formats
      like `stats` and `cycles`

    * `--format` - can be set to one of:

      * `pretty` - prints the graph to the terminal using Unicode characters.
        Each prints each file followed by the files it depends on. This is the
        default except on Windows;

      * `plain` - the same as pretty except ASCII characters are used instead of
        Unicode characters. This is the default on Windows;

      * `stats` - prints general statistics about the graph;

      * `cycles` - prints all cycles in the graph;

      * `dot` - produces a DOT graph description in `xref_graph.dot` in the
        current directory. Warning: this will override any previously generated file

  The `--source` and `--sink` options are particularly useful when trying to understand
  how the modules in a particular file interact with the whole system. You can combine
  those options with `--label` and `--only-nodes` to get all files that exhibit a certain
  property, for example:

      # To get the tree that depend on lib/foo.ex at compile time
      mix xref graph --label compile --sink lib/foo.ex

      # To get all files that depend on lib/foo.ex at compile time
      mix xref graph --label compile --sink lib/foo.ex --only-nodes

      # To get all paths between two files
      mix xref graph --source lib/foo.ex --sink lib/bar.ex

      # To show general statistics about the graph
      mix xref graph --format stats

      # To limit statistics only to certain labels
      mix xref graph --format stats --label compile

  #### Understanding the printed graph

  When `mix xref graph` runs, it will print a tree of the following
  format:

      lib/a.ex
      `-- lib/b.ex (compile)
          `-- lib/c.ex

  This tree means that `lib/a.ex` depends on `lib/b.ex` at compile
  time which then depends on `lib/c.ex` at runtime. This is often
  problematic because if `lib/c.ex` changes, `lib/a.ex` also has to
  recompile due to this indirect compile time dependency.

  This interpretation is the same regardless if `--source` or `--sink`
  flags are used. For example, if we use the `--sink lib/c.ex` flag,
  we would see the same tree:

      lib/a.ex
      `-- lib/b.ex (compile)
          `-- lib/c.ex

  If the `--label compile` flag is given with `--sink`, then `lib/c.ex`
  won't be shown, because no module has a compile time dependency on
  `lib/c.ex` but `lib/a.ex` still has an indirect compile time dependency
  on `lib/c.ex` via `lib/b.ex`:

      lib/a.ex
      `-- lib/b.ex (compile)

  Therefore, using a combination of `--sink` with `--label` is useful to
  find all files that will change once the sink changes, alongside the
  transitive dependencies that will cause said recompilations.

  #### Dependencies types

  Elixir tracks three types of dependencies between modules: compile,
  exports, and runtime. If a module has a compile time dependency on
  another module, the caller module has to be recompiled whenever the
  callee changes. Compile-time dependencies are typically added when
  using macros or when invoking functions in the module body (outside
  of functions).

  Exports dependencies are compile time dependencies on the module API,
  namely structs and its public definitions. For example, if you import
  a module but only use its functions, it is an export dependency. If
  you use a struct, it is an export dependency too. Export dependencies
  are only recompiled if the module API changes. Note, however, that compile
  time dependencies have higher precedence than exports. Therefore if
  you import a module and use its macros, it is a compile time dependency.

  Runtime dependencies are added whenever you invoke another module
  inside a function. Modules with runtime dependencies do not have
  to be compiled when the callee changes, unless there is a transitive
  compile or export time dependency between them.

  ## Shared options

  Those options are shared across all modes:

    * `--include-siblings` - includes dependencies that have `:in_umbrella` set
      to true in the current project in the reports. This can be used to find
      callers or to analyze graphs between projects

    * `--no-compile` - does not compile even if files require compilation

    * `--no-deps-check` - does not check dependencies

    * `--no-archives-check` - does not check archives

    * `--no-elixir-version-check` - does not check the Elixir version from mix.exs

  """

  @switches [
    abort_if_any: :boolean,
    archives_check: :boolean,
    compile: :boolean,
    deps_check: :boolean,
    elixir_version_check: :boolean,
    exclude: :keep,
    format: :string,
    include_siblings: :boolean,
    label: :string,
    only_nodes: :boolean,
    only_direct: :boolean,
    sink: :string,
    source: :string,
    min_cycle_size: :integer
  ]

  @impl true
  def run(args) do
    {opts, args} = OptionParser.parse!(args, strict: @switches)
    Mix.Task.run("compile", args)
    Mix.Task.reenable("xref")

    case args do
      ["callers", callee] ->
        callers(callee, opts)

      ["graph"] ->
        graph(opts)

      # TODO: Remove on v2.0
      ["deprecated"] ->
        Mix.shell().error(
          "The deprecated check has been moved to the compiler and has no effect now"
        )

      # TODO: Remove on v2.0
      ["unreachable"] ->
        Mix.shell().error(
          "The unreachable check has been moved to the compiler and has no effect now"
        )

      _ ->
        Mix.raise("xref doesn't support this command. For more information run \"mix help xref\"")
    end
  end

  @doc """
  Returns a list of information of all the runtime function calls in the project.

  Each item in the list is a map with the following keys:

    * `:callee` - a tuple containing the module, function, and arity of the call
    * `:line` - an integer representing the line where the function is called
    * `:file` - a binary representing the file where the function is called
    * `:caller_module` - the module where the function is called

  This function returns an empty list when used at the root of an umbrella
  project because there is no compile manifest to extract the function call
  information from. To get the function calls of each child in an umbrella,
  execute the function at the root of each individual application.
  """
  # TODO: Remove on v2.0
  @doc deprecated: "Use compilation tracers described in the Code module"
  @spec calls(keyword()) :: [
          %{
            callee: {module(), atom(), arity()},
            line: integer(),
            file: String.t()
          }
        ]
  def calls(opts \\ []) do
    for manifest <- manifests(opts),
        source(source: source, modules: modules) <- read_manifest(manifest) |> elem(1),
        module <- modules,
        call <- collect_calls(source, module),
        do: call
  end

  defp collect_calls(source, module) do
    with [_ | _] = path <- :code.which(module),
         {:ok, {_, [debug_info: debug_info]}} <- :beam_lib.chunks(path, [:debug_info]),
         {:debug_info_v1, backend, data} <- debug_info,
         {:ok, %{definitions: defs}} <- backend.debug_info(:elixir_v1, module, data, []),
         do: walk_definitions(module, source, defs)
  end

  defp walk_definitions(module, file, definitions) do
    state = %{
      file: file,
      module: module,
      calls: []
    }

    state = Enum.reduce(definitions, state, &walk_definition/2)
    state.calls
  end

  defp walk_definition({_function, _kind, meta, clauses}, state) do
    with_file_meta(state, meta, fn state ->
      Enum.reduce(clauses, state, &walk_clause/2)
    end)
  end

  defp with_file_meta(%{file: original_file} = state, meta, fun) do
    case Keyword.fetch(meta, :file) do
      {:ok, {meta_file, _}} ->
        state = fun.(%{state | file: meta_file})
        %{state | file: original_file}

      :error ->
        fun.(state)
    end
  end

  defp walk_clause({_meta, args, _guards, body}, state) do
    state = walk_expr(args, state)
    walk_expr(body, state)
  end

  # &Mod.fun/arity
  defp walk_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, state)
       when is_atom(module) and is_atom(fun) do
    add_call(module, fun, arity, meta, state)
  end

  # Mod.fun(...)
  defp walk_expr({{:., _, [module, fun]}, meta, args}, state)
       when is_atom(module) and is_atom(fun) do
    state = add_call(module, fun, length(args), meta, state)
    walk_expr(args, state)
  end

  # %Module{...}
  defp walk_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
       when is_atom(module) and is_list(args) do
    state = add_call(module, :__struct__, 0, meta, state)
    walk_expr(args, state)
  end

  # Function call
  defp walk_expr({left, _meta, right}, state) when is_list(right) do
    state = walk_expr(right, state)
    walk_expr(left, state)
  end

  # {x, y}
  defp walk_expr({left, right}, state) do
    state = walk_expr(right, state)
    walk_expr(left, state)
  end

  # [...]
  defp walk_expr(list, state) when is_list(list) do
    Enum.reduce(list, state, &walk_expr/2)
  end

  defp walk_expr(_other, state) do
    state
  end

  defp add_call(module, fun, arity, meta, state) do
    call = %{
      callee: {module, fun, arity},
      caller_module: state.module,
      file: state.file,
      line: meta[:line]
    }

    %{state | calls: [call | state.calls]}
  end

  ## Modes

  defp callers(callee, opts) do
    module = parse_callee(callee)

    file_callers =
      for source <- sources(opts),
          reference = reference(module, source),
          do: {source(source, :source), reference}

    for {file, type} <- Enum.sort(file_callers) do
      Mix.shell().info([file, " (", type, ")"])
    end

    :ok
  end

  defp graph(opts) do
    filter = label_filter(opts[:label])

    {direct_filter, transitive_filter} =
      if opts[:only_direct], do: {filter, :all}, else: {:all, filter}

    write_graph(file_references(direct_filter, opts), transitive_filter, opts)
    :ok
  end

  ## Callers

  defp parse_callee(callee) do
    case Mix.Utils.parse_mfa(callee) do
      {:ok, [module]} -> module
      _ -> Mix.raise("xref callers MODULE expects a MODULE, got: " <> callee)
    end
  end

  defp reference(module, source) do
    cond do
      module in source(source, :compile_references) -> "compile"
      module in source(source, :export_references) -> "export"
      module in source(source, :runtime_references) -> "runtime"
      true -> nil
    end
  end

  ## Graph

  defp excluded(opts) do
    opts
    |> Keyword.get_values(:exclude)
    |> Enum.flat_map(&[{&1, nil}, {&1, :compile}, {&1, :export}])
  end

  defp label_filter(nil), do: :all
  defp label_filter("compile"), do: :compile
  defp label_filter("export"), do: :export
  defp label_filter("runtime"), do: nil
  defp label_filter(other), do: Mix.raise("unknown --label #{other}")

  defp file_references(filter, opts) do
    module_sources =
      for manifest_path <- manifests(opts),
          {manifest_modules, manifest_sources} = read_manifest(manifest_path),
          module(module: module, sources: sources) <- manifest_modules,
          source <- sources,
          source = Enum.find(manifest_sources, &match?(source(source: ^source), &1)),
          do: {module, source}

    all_modules = MapSet.new(module_sources, &elem(&1, 0))

    Map.new(module_sources, fn {current, source} ->
      source(
        runtime_references: runtime,
        export_references: exports,
        compile_references: compile,
        source: file
      ) = source

      compile_references =
        modules_to_nodes(compile, :compile, current, source, module_sources, all_modules, filter)

      export_references =
        modules_to_nodes(exports, :export, current, source, module_sources, all_modules, filter)

      runtime_references =
        modules_to_nodes(runtime, nil, current, source, module_sources, all_modules, filter)

      references =
        runtime_references
        |> Map.merge(export_references)
        |> Map.merge(compile_references)
        |> Enum.to_list()

      {file, references}
    end)
  end

  defp modules_to_nodes(_, label, _, _, _, _, filter) when filter != :all and label != filter do
    %{}
  end

  defp modules_to_nodes(modules, label, current, source, module_sources, all_modules, _filter) do
    for module <- modules,
        module != current,
        module in all_modules,
        module_sources[module] != source,
        do: {source(module_sources[module], :source), label},
        into: %{}
  end

  defp write_graph(file_references, filter, opts) do
    excluded = excluded(opts)
    source = opts[:source]
    sink = opts[:sink]

    if source && is_nil(file_references[source]) do
      Mix.raise("Source could not be found: #{source}")
    end

    if sink && is_nil(file_references[sink]) do
      Mix.raise("Sink could not be found: #{sink}")
    end

    file_references =
      if sink = opts[:sink] do
        filter_for_sink(file_references, sink, filter)
      else
        filter_for_source(file_references, filter)
      end

    roots =
      if source = opts[:source] do
        %{source => nil}
      else
        file_references
        |> Map.delete(opts[:sink])
        |> Enum.map(&{elem(&1, 0), nil})
        |> Kernel.--(excluded)
        |> Map.new()
      end

    callback = fn {file, type} ->
      children = if opts[:only_nodes], do: [], else: Map.get(file_references, file, [])
      type = type && "(#{type})"
      {{file, type}, Enum.sort(children -- excluded)}
    end

    case opts[:format] do
      "dot" ->
        Mix.Utils.write_dot_graph!(
          "xref_graph.dot",
          "xref graph",
          Enum.sort(roots),
          callback,
          opts
        )

        """
        Generated "xref_graph.dot" in the current directory. To generate a PNG:

           dot -Tpng xref_graph.dot -o xref_graph.png

        For more options see http://www.graphviz.org/.
        """
        |> String.trim_trailing()
        |> Mix.shell().info()

      "stats" ->
        print_stats(file_references, opts)

      "cycles" ->
        print_cycles(file_references, opts)

      _ ->
        Mix.Utils.print_tree(Enum.sort(roots), callback, opts)
    end
  end

  defp filter_for_source(file_references, :all), do: file_references

  defp filter_for_source(file_references, filter) do
    Enum.reduce(file_references, %{}, fn {key, _}, acc ->
      {children, _} = filter_for_source(file_references, key, %{}, %{}, filter)
      Map.put(acc, key, children |> Map.delete(key) |> Map.to_list())
    end)
  end

  defp filter_for_source(references, key, acc, seen, filter) do
    nodes = references[key]

    if is_nil(nodes) || seen[key] do
      {acc, seen}
    else
      seen = Map.put(seen, key, true)

      Enum.reduce(nodes, {acc, seen}, fn {child_key, type}, {acc, seen} ->
        if type == filter do
          {Map.put(acc, child_key, type), Map.put(seen, child_key, true)}
        else
          filter_for_source(references, child_key, acc, seen, filter)
        end
      end)
    end
  end

  defp filter_for_sink(file_references, sink, filter) do
    fun = if filter == :all, do: fn _ -> true end, else: fn type -> type == filter end

    file_references
    |> invert_references(fn _ -> true end)
    |> depends_on_sink([{sink, nil}], %{})
    |> invert_references(fun)
  end

  defp depends_on_sink(file_references, new_nodes, acc) do
    Enum.reduce(new_nodes, acc, fn {new_node_name, _type}, acc ->
      new_nodes = file_references[new_node_name]

      if acc[new_node_name] || !new_nodes do
        acc
      else
        depends_on_sink(file_references, new_nodes, Map.put(acc, new_node_name, new_nodes))
      end
    end)
  end

  defp invert_references(file_references, fun) do
    Enum.reduce(file_references, %{}, fn {file, references}, acc ->
      Enum.reduce(references, acc, fn {reference, type}, acc ->
        if fun.(type) do
          Map.update(acc, reference, [{file, type}], &[{file, type} | &1])
        else
          acc
        end
      end)
    end)
  end

  defp print_stats(references, opts) do
    with_digraph(references, fn graph ->
      shell = Mix.shell()

      counters =
        Enum.reduce(references, %{compile: 0, export: 0, nil: 0}, fn {_, deps}, acc ->
          Enum.reduce(deps, acc, fn {_, value}, acc ->
            Map.update!(acc, value, &(&1 + 1))
          end)
        end)

      shell.info("Tracked files: #{map_size(references)} (nodes)")
      shell.info("Compile dependencies: #{counters.compile} (edges)")
      shell.info("Exports dependencies: #{counters.export} (edges)")
      shell.info("Runtime dependencies: #{counters.nil} (edges)")
      shell.info("Cycles: #{length(cycles(graph, opts))}")

      outgoing =
        references
        |> Enum.map(fn {file, _} -> {:digraph.out_degree(graph, file), file} end)
        |> Enum.sort(:desc)
        |> Enum.take(10)

      shell.info("\nTop #{length(outgoing)} files with most outgoing dependencies:")
      for {count, file} <- outgoing, do: shell.info("  * #{file} (#{count})")

      incoming =
        references
        |> Enum.map(fn {file, _} -> {:digraph.in_degree(graph, file), file} end)
        |> Enum.sort(:desc)
        |> Enum.take(10)

      shell.info("\nTop #{length(incoming)} files with most incoming dependencies:")
      for {count, file} <- incoming, do: shell.info("  * #{file} (#{count})")
    end)
  end

  defp with_digraph(references, callback) do
    graph = :digraph.new()

    try do
      for {file, _} <- references do
        :digraph.add_vertex(graph, file)
      end

      for {file, deps} <- references, {dep, label} <- deps do
        :digraph.add_edge(graph, file, dep, label)
      end

      callback.(graph)
    after
      :digraph.delete(graph)
    end
  end

  defp cycles(graph, opts) do
    cycles =
      graph
      |> :digraph_utils.cyclic_strong_components()
      |> Enum.reduce([], &inner_cycles(graph, &1, &2))
      |> Enum.map(&{length(&1), &1})

    if min = opts[:min_cycle_size], do: Enum.filter(cycles, &(elem(&1, 0) > min)), else: cycles
  end

  defp inner_cycles(_graph, [], acc), do: acc

  defp inner_cycles(graph, [v | vertices], acc) do
    cycle = :digraph.get_cycle(graph, v)
    inner_cycles(graph, vertices -- cycle, [cycle | acc])
  end

  defp print_cycles(references, opts) do
    with_digraph(references, fn graph ->
      shell = Mix.shell()

      case graph |> cycles(opts) |> Enum.sort(:desc) do
        [] ->
          shell.info("No cycles found")

        cycles ->
          shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n")

          for {length, cycle} <- cycles do
            shell.info("Cycle of length #{length}:\n")

            for node <- cycle do
              shell.info("    " <> node)
            end

            shell.info("")
          end
      end
    end)
  end

  ## Helpers

  defp sources(opts) do
    for manifest <- manifests(opts),
        source() = source <- read_manifest(manifest) |> elem(1),
        do: source
  end

  defp manifests(opts) do
    siblings =
      if opts[:include_siblings] do
        for %{scm: Mix.SCM.Path, opts: opts} <- Mix.Dep.cached(),
            opts[:in_umbrella],
            do: Path.join([opts[:build], ".mix", @manifest])
      else
        []
      end

    [Path.join(Mix.Project.manifest_path(), @manifest) | siblings]
  end
end

defmodule Mix.Tasks.Xref do
  use Mix.Task

  import Mix.Compilers.Elixir,
    only: [read_manifest: 1, source: 0, source: 1, source: 2, module: 1]

  @shortdoc "Prints cross reference information"
  @recursive true
  @manifest "compile.elixir"

  @moduledoc """
  Prints cross reference information between modules.

  This task is automatically reenabled, so you can print information
  multiple times in the same Mix invocation.

  ## Xref modes

  The `xref` task expects a mode as first argument:

      mix xref MODE

  All available modes are discussed below.

  ### callers CALLEE

  Prints all callers of the given `MODULE`. Example:

      mix xref callers MyMod

  ### graph

  Prints a file dependency graph where an edge from `A` to `B` indicates
  that `A` (source) depends on `B` (sink).

      mix xref graph --format stats

  The following options are accepted:

    * `--exclude` - paths to exclude

    * `--label` - only shows relationships with the given label.
      By default, it keeps all labels that are transitive.
      The labels are "compile", "export" and "runtime". See
      "Dependencies types" section below

    * `--only-nodes` - only shows the node names (no edges)

    * `--only-direct` - the `--label` option will restrict itself
      to only direct dependencies instead of transitive ones

    * `--source` - displays all files that the given source file
      references (directly or indirectly)

    * `--sink` - displays all files that reference the given file
      (directly or indirectly)

    * `--min-cycle-size` - controls the minimum cycle size on formats
      like `stats` and `cycles`

    * `--format` - can be set to one of:

      * `pretty` - prints the graph to the terminal using Unicode characters.
        Each prints each file followed by the files it depends on. This is the
        default except on Windows;

      * `plain` - the same as pretty except ASCII characters are used instead of
        Unicode characters. This is the default on Windows;

      * `stats` - prints general statistics about the graph;

      * `cycles` - prints all cycles in the graph;

      * `dot` - produces a DOT graph description in `xref_graph.dot` in the
        current directory. Warning: this will override any previously generated file

  The `--source` and `--sink` options are particularly useful when trying to understand
  how the modules in a particular file interact with the whole system. You can combine
  those options with `--label` and `--only-nodes` to get all files that exhibit a certain
  property, for example:

      # To get the tree that depend on lib/foo.ex at compile time
      mix xref graph --label compile --sink lib/foo.ex

      # To get all files that depend on lib/foo.ex at compile time
      mix xref graph --label compile --sink lib/foo.ex --only-nodes

      # To get all paths between two files
      mix xref graph --source lib/foo.ex --sink lib/bar.ex

      # To show general statistics about the graph
      mix xref graph --format stats

      # To limit statistics only to certain labels
      mix xref graph --format stats --label compile

  #### Understanding the printed graph

  When `mix xref graph` runs, it will print a tree of the following
  format:

      lib/a.ex
      `-- lib/b.ex (compile)
          `-- lib/c.ex

  This tree means that `lib/a.ex` depends on `lib/b.ex` at compile
  time which then depends on `lib/c.ex` at runtime. This is often
  problematic because if `lib/c.ex` changes, `lib/a.ex` also has to
  recompile due to this indirect compile time dependency.

  This interpretation is the same regardless if `--source` or `--sink`
  flags are used. For example, if we use the `--sink lib/c.ex` flag,
  we would see the same tree:

      lib/a.ex
      `-- lib/b.ex (compile)
          `-- lib/c.ex

  If the `--label compile` flag is given with `--sink`, then `lib/c.ex`
  won't be shown, because no module has a compile time dependency on
  `lib/c.ex` but `lib/a.ex` still has an indirect compile time dependency
  on `lib/c.ex` via `lib/b.ex`:

      lib/a.ex
      `-- lib/b.ex (compile)

  Therefore, using a combination of `--sink` with `--label` is useful to
  find all files that will change once the sink changes, alongside the
  transitive dependencies that will cause said recompilations.

  #### Dependencies types

  ELixir tracks three types of dependencies between modules: compile,
  exports, and runtime. If a module has a compile time dependency on
  another module, the caller module has to be recompiled whenever the
  callee changes. Compile-time dependencies are typically added when
  using macros or when invoking functions in the module body (outside
  of functions).

  Exports dependencies are compile time dependencies on the module API,
  namely structs and its public definitions. For example, if you import
  a module but only use its functions, it is an export dependency. If
  you use a struct, it is an export dependency too. Export dependencies
  are only recompiled if the module API changes. Note, however, that compile
  time dependencies have higher precedence than exports. Therefore if
  you import a module and use its macros, it is a compile time dependency.

  Runtime dependencies are added whenever you invoke another module
  inside a function. Modules with runtime dependencies do not have
  to be compiled when the callee changes, unless there is a transitive
  compile or export time dependency between them.

  ## Shared options

  Those options are shared across all modes:

    * `--include-siblings` - includes dependencies that have `:in_umbrella` set
      to true in the current project in the reports. This can be used to find
      callers or to analyze graphs between projects

    * `--no-compile` - does not compile even if files require compilation

    * `--no-deps-check` - does not check dependencies

    * `--no-archives-check` - does not check archives

    * `--no-elixir-version-check` - does not check the Elixir version from mix.exs

  """

  @switches [
    abort_if_any: :boolean,
    archives_check: :boolean,
    compile: :boolean,
    deps_check: :boolean,
    elixir_version_check: :boolean,
    exclude: :keep,
    format: :string,
    include_siblings: :boolean,
    label: :string,
    only_nodes: :boolean,
    only_direct: :boolean,
    sink: :string,
    source: :string,
    min_cycle_size: :integer
  ]

  @impl true
  def run(args) do
    {opts, args} = OptionParser.parse!(args, strict: @switches)
    Mix.Task.run("compile", args)
    Mix.Task.reenable("xref")

    case args do
      ["callers", callee] ->
        callers(callee, opts)

      ["graph"] ->
        graph(opts)

      # TODO: Remove on v2.0
      ["deprecated"] ->
        Mix.shell().error(
          "The deprecated check has been moved to the compiler and has no effect now"
        )

      # TODO: Remove on v2.0
      ["unreachable"] ->
        Mix.shell().error(
          "The unreachable check has been moved to the compiler and has no effect now"
        )

      _ ->
        Mix.raise("xref doesn't support this command. For more information run \"mix help xref\"")
    end
  end

  @doc """
  Returns a list of information of all the runtime function calls in the project.

  Each item in the list is a map with the following keys:

    * `:callee` - a tuple containing the module, function, and arity of the call
    * `:line` - an integer representing the line where the function is called
    * `:file` - a binary representing the file where the function is called
    * `:caller_module` - the module where the function is called

  This function returns an empty list when used at the root of an umbrella
  project because there is no compile manifest to extract the function call
  information from. To get the function calls of each child in an umbrella,
  execute the function at the root of each individual application.
  """
  # TODO: Remove on v2.0
  @doc deprecated: "Use compilation tracers described in the Code module"
  @spec calls(keyword()) :: [
          %{
            callee: {module(), atom(), arity()},
            line: integer(),
            file: String.t()
          }
        ]
  def calls(opts \\ []) do
    for manifest <- manifests(opts),
        source(source: source, modules: modules) <- read_manifest(manifest) |> elem(1),
        module <- modules,
        call <- collect_calls(source, module),
        do: call
  end

  defp collect_calls(source, module) do
    with [_ | _] = path <- :code.which(module),
         {:ok, {_, [debug_info: debug_info]}} <- :beam_lib.chunks(path, [:debug_info]),
         {:debug_info_v1, backend, data} <- debug_info,
         {:ok, %{definitions: defs}} <- backend.debug_info(:elixir_v1, module, data, []),
         do: walk_definitions(module, source, defs)
  end

  defp walk_definitions(module, file, definitions) do
    state = %{
      file: file,
      module: module,
      calls: []
    }

    state = Enum.reduce(definitions, state, &walk_definition/2)
    state.calls
  end

  defp walk_definition({_function, _kind, meta, clauses}, state) do
    with_file_meta(state, meta, fn state ->
      Enum.reduce(clauses, state, &walk_clause/2)
    end)
  end

  defp with_file_meta(%{file: original_file} = state, meta, fun) do
    case Keyword.fetch(meta, :file) do
      {:ok, {meta_file, _}} ->
        state = fun.(%{state | file: meta_file})
        %{state | file: original_file}

      :error ->
        fun.(state)
    end
  end

  defp walk_clause({_meta, args, _guards, body}, state) do
    state = walk_expr(args, state)
    walk_expr(body, state)
  end

  # &Mod.fun/arity
  defp walk_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, state)
       when is_atom(module) and is_atom(fun) do
    add_call(module, fun, arity, meta, state)
  end

  # Mod.fun(...)
  defp walk_expr({{:., _, [module, fun]}, meta, args}, state)
       when is_atom(module) and is_atom(fun) do
    state = add_call(module, fun, length(args), meta, state)
    walk_expr(args, state)
  end

  # %Module{...}
  defp walk_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
       when is_atom(module) and is_list(args) do
    state = add_call(module, :__struct__, 0, meta, state)
    walk_expr(args, state)
  end

  # Function call
  defp walk_expr({left, _meta, right}, state) when is_list(right) do
    state = walk_expr(right, state)
    walk_expr(left, state)
  end

  # {x, y}
  defp walk_expr({left, right}, state) do
    state = walk_expr(right, state)
    walk_expr(left, state)
  end

  # [...]
  defp walk_expr(list, state) when is_list(list) do
    Enum.reduce(list, state, &walk_expr/2)
  end

  defp walk_expr(_other, state) do
    state
  end

  defp add_call(module, fun, arity, meta, state) do
    call = %{
      callee: {module, fun, arity},
      caller_module: state.module,
      file: state.file,
      line: meta[:line]
    }

    %{state | calls: [call | state.calls]}
  end

  ## Modes

  defp callers(callee, opts) do
    module = parse_callee(callee)

    file_callers =
      for source <- sources(opts),
          reference = reference(module, source),
          do: {source(source, :source), reference}

    for {file, type} <- Enum.sort(file_callers) do
      Mix.shell().info([file, " (", type, ")"])
    end

    :ok
  end

  defp graph(opts) do
    filter = label_filter(opts[:label])

    {direct_filter, transitive_filter} =
      if opts[:only_direct], do: {filter, :all}, else: {:all, filter}

    write_graph(file_references(direct_filter, opts), transitive_filter, opts)
    :ok
  end

  ## Callers

  defp parse_callee(callee) do
    case Mix.Utils.parse_mfa(callee) do
      {:ok, [module]} -> module
      _ -> Mix.raise("xref callers MODULE expects a MODULE, got: " <> callee)
    end
  end

  defp reference(module, source) do
    cond do
      module in source(source, :compile_references) -> "compile"
      module in source(source, :export_references) -> "export"
      module in source(source, :runtime_references) -> "runtime"
      true -> nil
    end
  end

  ## Graph

  defp excluded(opts) do
    opts
    |> Keyword.get_values(:exclude)
    |> Enum.flat_map(&[{&1, nil}, {&1, :compile}, {&1, :export}])
  end

  defp label_filter(nil), do: :all
  defp label_filter("compile"), do: :compile
  defp label_filter("export"), do: :export
  defp label_filter("runtime"), do: nil
  defp label_filter(other), do: Mix.raise("unknown --label #{other}")

  defp file_references(filter, opts) do
    module_sources =
      for manifest_path <- manifests(opts),
          {manifest_modules, manifest_sources} = read_manifest(manifest_path),
          module(module: module, sources: sources) <- manifest_modules,
          source <- sources,
          source = Enum.find(manifest_sources, &match?(source(source: ^source), &1)),
          do: {module, source}

    all_modules = MapSet.new(module_sources, &elem(&1, 0))

    Map.new(module_sources, fn {current, source} ->
      source(
        runtime_references: runtime,
        export_references: exports,
        compile_references: compile,
        source: file
      ) = source

      compile_references =
        modules_to_nodes(compile, :compile, current, source, module_sources, all_modules, filter)

      export_references =
        modules_to_nodes(exports, :export, current, source, module_sources, all_modules, filter)

      runtime_references =
        modules_to_nodes(runtime, nil, current, source, module_sources, all_modules, filter)

      references =
        runtime_references
        |> Map.merge(export_references)
        |> Map.merge(compile_references)
        |> Enum.to_list()

      {file, references}
    end)
  end

  defp modules_to_nodes(_, label, _, _, _, _, filter) when filter != :all and label != filter do
    %{}
  end

  defp modules_to_nodes(modules, label, current, source, module_sources, all_modules, _filter) do
    for module <- modules,
        module != current,
        module in all_modules,
        module_sources[module] != source,
        do: {source(module_sources[module], :source), label},
        into: %{}
  end

  defp write_graph(file_references, filter, opts) do
    excluded = excluded(opts)
    source = opts[:source]
    sink = opts[:sink]

    if source && is_nil(file_references[source]) do
      Mix.raise("Source could not be found: #{source}")
    end

    if sink && is_nil(file_references[sink]) do
      Mix.raise("Sink could not be found: #{sink}")
    end

    file_references =
      if sink = opts[:sink] do
        filter_for_sink(file_references, sink, filter)
      else
        filter_for_source(file_references, filter)
      end

    roots =
      if source = opts[:source] do
        %{source => nil}
      else
        file_references
        |> Map.delete(opts[:sink])
        |> Enum.map(&{elem(&1, 0), nil})
        |> Kernel.--(excluded)
        |> Map.new()
      end

    callback = fn {file, type} ->
      children = if opts[:only_nodes], do: [], else: Map.get(file_references, file, [])
      type = type && "(#{type})"
      {{file, type}, Enum.sort(children -- excluded)}
    end

    case opts[:format] do
      "dot" ->
        Mix.Utils.write_dot_graph!(
          "xref_graph.dot",
          "xref graph",
          Enum.sort(roots),
          callback,
          opts
        )

        """
        Generated "xref_graph.dot" in the current directory. To generate a PNG:

           dot -Tpng xref_graph.dot -o xref_graph.png

        For more options see http://www.graphviz.org/.
        """
        |> String.trim_trailing()
        |> Mix.shell().info()

      "stats" ->
        print_stats(file_references, opts)

      "cycles" ->
        print_cycles(file_references, opts)

      _ ->
        Mix.Utils.print_tree(Enum.sort(roots), callback, opts)
    end
  end

  defp filter_for_source(file_references, :all), do: file_references

  defp filter_for_source(file_references, filter) do
    Enum.reduce(file_references, %{}, fn {key, _}, acc ->
      {children, _} = filter_for_source(file_references, key, %{}, %{}, filter)
      Map.put(acc, key, children |> Map.delete(key) |> Map.to_list())
    end)
  end

  defp filter_for_source(references, key, acc, seen, filter) do
    nodes = references[key]

    if is_nil(nodes) || seen[key] do
      {acc, seen}
    else
      seen = Map.put(seen, key, true)

      Enum.reduce(nodes, {acc, seen}, fn {child_key, type}, {acc, seen} ->
        if type == filter do
          {Map.put(acc, child_key, type), Map.put(seen, child_key, true)}
        else
          filter_for_source(references, child_key, acc, seen, filter)
        end
      end)
    end
  end

  defp filter_for_sink(file_references, sink, filter) do
    fun = if filter == :all, do: fn _ -> true end, else: fn type -> type == filter end

    file_references
    |> invert_references(fn _ -> true end)
    |> depends_on_sink([{sink, nil}], %{})
    |> invert_references(fun)
  end

  defp depends_on_sink(file_references, new_nodes, acc) do
    Enum.reduce(new_nodes, acc, fn {new_node_name, _type}, acc ->
      new_nodes = file_references[new_node_name]

      if acc[new_node_name] || !new_nodes do
        acc
      else
        depends_on_sink(file_references, new_nodes, Map.put(acc, new_node_name, new_nodes))
      end
    end)
  end

  defp invert_references(file_references, fun) do
    Enum.reduce(file_references, %{}, fn {file, references}, acc ->
      Enum.reduce(references, acc, fn {reference, type}, acc ->
        if fun.(type) do
          Map.update(acc, reference, [{file, type}], &[{file, type} | &1])
        else
          acc
        end
      end)
    end)
  end

  defp print_stats(references, opts) do
    with_digraph(references, fn graph ->
      shell = Mix.shell()

      counters =
        Enum.reduce(references, %{compile: 0, export: 0, nil: 0}, fn {_, deps}, acc ->
          Enum.reduce(deps, acc, fn {_, value}, acc ->
            Map.update!(acc, value, &(&1 + 1))
          end)
        end)

      shell.info("Tracked files: #{map_size(references)} (nodes)")
      shell.info("Compile dependencies: #{counters.compile} (edges)")
      shell.info("Exports dependencies: #{counters.export} (edges)")
      shell.info("Runtime dependencies: #{counters.nil} (edges)")
      shell.info("Cycles: #{length(cycles(graph, opts))}")

      outgoing =
        references
        |> Enum.map(fn {file, _} -> {:digraph.out_degree(graph, file), file} end)
        |> Enum.sort(:desc)
        |> Enum.take(10)

      shell.info("\nTop #{length(outgoing)} files with most outgoing dependencies:")
      for {count, file} <- outgoing, do: shell.info("  * #{file} (#{count})")

      incoming =
        references
        |> Enum.map(fn {file, _} -> {:digraph.in_degree(graph, file), file} end)
        |> Enum.sort(:desc)
        |> Enum.take(10)

      shell.info("\nTop #{length(incoming)} files with most incoming dependencies:")
      for {count, file} <- incoming, do: shell.info("  * #{file} (#{count})")
    end)
  end

  defp with_digraph(references, callback) do
    graph = :digraph.new()

    try do
      for {file, _} <- references do
        :digraph.add_vertex(graph, file)
      end

      for {file, deps} <- references, {dep, label} <- deps do
        :digraph.add_edge(graph, file, dep, label)
      end

      callback.(graph)
    after
      :digraph.delete(graph)
    end
  end

  defp cycles(graph, opts) do
    cycles =
      graph
      |> :digraph_utils.cyclic_strong_components()
      |> Enum.reduce([], &inner_cycles(graph, &1, &2))
      |> Enum.map(&{length(&1), &1})

    if min = opts[:min_cycle_size], do: Enum.filter(cycles, &(elem(&1, 0) > min)), else: cycles
  end

  defp inner_cycles(_graph, [], acc), do: acc

  defp inner_cycles(graph, [v | vertices], acc) do
    cycle = :digraph.get_cycle(graph, v)
    inner_cycles(graph, vertices -- cycle, [cycle | acc])
  end

  defp print_cycles(references, opts) do
    with_digraph(references, fn graph ->
      shell = Mix.shell()

      case graph |> cycles(opts) |> Enum.sort(:desc) do
        [] ->
          shell.info("No cycles found")

        cycles ->
          shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n")

          for {length, cycle} <- cycles do
            shell.info("Cycle of length #{length}:\n")

            for node <- cycle do
              shell.info("    " <> node)
            end

            shell.info("")
          end
      end
    end)
  end

  ## Helpers

  defp sources(opts) do
    for manifest <- manifests(opts),
        source() = source <- read_manifest(manifest) |> elem(1),
        do: source
  end

  defp manifests(opts) do
    siblings =
      if opts[:include_siblings] do
        for %{scm: Mix.SCM.Path, opts: opts} <- Mix.Dep.cached(),
            opts[:in_umbrella],
            do: Path.join([opts[:build], ".mix", @manifest])
      else
        []
      end

    [Path.join(Mix.Project.manifest_path(), @manifest) | siblings]
  end
end