<commit_before>defmodule ExUnit.Filters do
  alias ExUnit.FailuresManifest

  @moduledoc """
  Conveniences for parsing and evaluating filters.
  """

  @type t :: list({atom, Regex.t() | String.Chars.t()} | atom)

  @doc """
  Parses filters out of a path.

  Determines whether a given file path (supplied to ExUnit/Mix as arguments
  on the command line) includes a line number filter, and if so returns the
  appropriate ExUnit configuration options.
  """
  @spec parse_path(String.t()) :: {String.t(), any}
  def parse_path(file) do
    {paths, [line]} = file |> String.split(":") |> Enum.split(-1)

    case Integer.parse(line) do
      {_, ""} ->
        {Enum.join(paths, ":"), exclude: [:test], include: [line: line]}

      _ ->
        {file, []}
    end
  end

  @doc """
  Normalizes include and excludes to remove duplicates
  and keep precedence.

  ## Examples

      iex> ExUnit.Filters.normalize(nil, nil)
      {[], []}

      iex> ExUnit.Filters.normalize([:foo, :bar, :bar], [:foo, :baz])
      {[:foo, :bar], [:baz]}

      iex> ExUnit.Filters.normalize([foo: "true"], [:foo])
      {[foo: "true"], [:foo]}

      iex> ExUnit.Filters.normalize([:foo], [foo: "true"])
      {[:foo], []}

      iex> ExUnit.Filters.normalize([foo: "true"], [foo: true])
      {[foo: "true"], []}

      iex> ExUnit.Filters.normalize([foo: true], [foo: "true"])
      {[foo: true], []}

  """
  @spec normalize(t | nil, t | nil) :: {t, t}
  def normalize(include, exclude) do
    {include_atoms, include_tags} =
      include |> List.wrap() |> Enum.uniq() |> Enum.split_with(&is_atom/1)

    {exclude_atoms, exclude_tags} =
      exclude |> List.wrap() |> Enum.uniq() |> Enum.split_with(&is_atom/1)

    exclude_tags = Map.new(exclude_tags)

    exclude_included =
      for include_tag <- include_tags, key = has_tag(include_tag, exclude_tags), do: key

    exclude_tags =
      exclude_tags |> Map.drop(include_atoms) |> Map.drop(exclude_included) |> Map.to_list()

    {include_atoms ++ include_tags, (exclude_atoms -- include_atoms) ++ exclude_tags}
  end

  @doc """
  Parses the given filters, as one would receive from the command line.

  ## Examples

      iex> ExUnit.Filters.parse(["foo:bar", "baz", "line:9", "bool:true"])
      [{:foo, "bar"}, :baz, {:line, "9"}, {:bool, "true"}]

  """
  @spec parse([String.t()]) :: t
  def parse(filters) do
    Enum.map(filters, fn filter ->
      case String.split(filter, ":", parts: 2) do
        [key, value] -> {String.to_atom(key), value}
        [key] -> String.to_atom(key)
      end
    end)
  end

  @doc """
  Returns a tuple containing useful information about test failures from the
  manifest. The tuple contains:

    * A set of files that contain tests that failed the last time they ran.
      The paths are absolute paths.
    * A set of test ids that failed the last time they ran

  """
  @spec failure_info(Path.t()) :: {MapSet.t(Path.t()), MapSet.t(FailuresManifest.test_id())}
  def failure_info(manifest_file) do
    manifest = FailuresManifest.read(manifest_file)
    {FailuresManifest.files_with_failures(manifest), FailuresManifest.failed_test_ids(manifest)}
  end

  @doc """
  Evaluates the `include` and `exclude` filters against the given `tags` to
  determine if tests should be skipped or excluded.

  Some filters, like `:line`, may require the whole test collection to
  find the closest line, that's why it must also be passed as argument.

  Filters can either be a regular expression or any data structure
  that implements to `String.Chars`, which is invoked before comparing
  the filter with the tag value.

  ## Examples

      iex> ExUnit.Filters.eval([foo: "bar"], [:foo], %{foo: "bar"}, [])
      :ok

      iex> ExUnit.Filters.eval([foo: "bar"], [:foo], %{foo: "baz"}, [])
      {:excluded, "due to foo filter"}

  """<commit_msg>Improvements in documentation of ExUnit.Filters (#8185)<commit_after>defmodule ExUnit.Filters do
  alias ExUnit.FailuresManifest

  @moduledoc """
  Conveniences for parsing and evaluating filters.
  """

  @type t :: list({atom, Regex.t() | String.Chars.t()} | atom)

  @doc """
  Parses filters out of a path.

  Determines whether a given file path (supplied to ExUnit/Mix as arguments
  on the command line) includes a line number filter, and if so returns the
  appropriate ExUnit configuration options.
  """
  @spec parse_path(String.t()) :: {String.t(), any}
  def parse_path(file) do
    {paths, [line]} = file |> String.split(":") |> Enum.split(-1)

    case Integer.parse(line) do
      {_, ""} ->
        {Enum.join(paths, ":"), exclude: [:test], include: [line: line]}

      _ ->
        {file, []}
    end
  end

  @doc """
  Normalizes `include` and `exclude` filters to remove duplicates
  and keep precedence.

  ## Examples

      iex> ExUnit.Filters.normalize(nil, nil)
      {[], []}

      iex> ExUnit.Filters.normalize([:foo, :bar, :bar], [:foo, :baz])
      {[:foo, :bar], [:baz]}

      iex> ExUnit.Filters.normalize([foo: "true"], [:foo])
      {[foo: "true"], [:foo]}

      iex> ExUnit.Filters.normalize([:foo], [foo: "true"])
      {[:foo], []}

      iex> ExUnit.Filters.normalize([foo: "true"], [foo: true])
      {[foo: "true"], []}

      iex> ExUnit.Filters.normalize([foo: true], [foo: "true"])
      {[foo: true], []}

  """
  @spec normalize(t | nil, t | nil) :: {t, t}
  def normalize(include, exclude) do
    {include_atoms, include_tags} =
      include |> List.wrap() |> Enum.uniq() |> Enum.split_with(&is_atom/1)

    {exclude_atoms, exclude_tags} =
      exclude |> List.wrap() |> Enum.uniq() |> Enum.split_with(&is_atom/1)

    exclude_tags = Map.new(exclude_tags)

    exclude_included =
      for include_tag <- include_tags, key = has_tag(include_tag, exclude_tags), do: key

    exclude_tags =
      exclude_tags |> Map.drop(include_atoms) |> Map.drop(exclude_included) |> Map.to_list()

    {include_atoms ++ include_tags, (exclude_atoms -- include_atoms) ++ exclude_tags}
  end

  @doc """
  Parses the given filters, as one would receive from the command line.

  ## Examples

      iex> ExUnit.Filters.parse(["foo:bar", "baz", "line:9", "bool:true"])
      [{:foo, "bar"}, :baz, {:line, "9"}, {:bool, "true"}]

  """
  @spec parse([String.t()]) :: t
  def parse(filters) do
    Enum.map(filters, fn filter ->
      case String.split(filter, ":", parts: 2) do
        [key, value] -> {String.to_atom(key), value}
        [key] -> String.to_atom(key)
      end
    end)
  end

  @doc """
  Returns a tuple containing useful information about test failures from the
  manifest. The tuple contains:

    * A set of files that contain tests that failed the last time they ran.
      The paths are absolute paths.
    * A set of test ids that failed the last time they ran

  """
  @spec failure_info(Path.t()) :: {MapSet.t(Path.t()), MapSet.t(FailuresManifest.test_id())}
  def failure_info(manifest_file) do
    manifest = FailuresManifest.read(manifest_file)
    {FailuresManifest.files_with_failures(manifest), FailuresManifest.failed_test_ids(manifest)}
  end

  @doc """
  Evaluates the `include` and `exclude` filters against the given `tags` to
  determine if tests should be skipped or excluded.

  Some filters, like `:line`, may require the whole test collection to
  find the closest line, that's why it must also be passed as an argument.

  Filters can either be a regular expression or any data structure
  that implements the `String.Chars` protocol, which is invoked before comparing
  the filter with the tag value.

  ## Examples

      iex> ExUnit.Filters.eval([foo: "bar"], [:foo], %{foo: "bar"}, [])
      :ok

      iex> ExUnit.Filters.eval([foo: "bar"], [:foo], %{foo: "baz"}, [])
      {:excluded, "due to foo filter"}

  """
{'fraction_non_alphanumeric': 0.15454310121667097, 'fraction_numerical': 0.0020709293295366298, 'mean_word_length': 3.307134894091416, 'pattern_counts': {'":': 6, '<': 5, '<?xml version=': 0, '>': 53, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '108477', 'n_tokens_mistral': 2642, 'n_tokens_neox': 2529, 'n_words': 748}