4.7 KiB · text History 91138c0
defmodule GitGud.Workflows.GlobTest do
  @moduledoc """
  Filter patterns (`branches:`, `tags:`, `paths:`) follow GitHub's cheat
  sheet, not shell globbing: a workflow copied from GitHub's docs has to
  claim the same refs and files here. `?` and `+` are quantifiers there, not
  wildcards.
  """
  use ExUnit.Case, async: true

  alias GitGud.Workflows.Glob

  doctest Glob

  defp m?(pattern, path), do: Glob.match?(pattern, path)

  test "* stays within one directory" do
    assert m?("*.md", "a.md")
    refute m?("*.md", "docs/a.md")
  end

  test "**/ matches any depth, including none" do
    assert m?("**/*.md", "a.md")
    assert m?("**/*.md", "docs/a.md")
    assert m?("**/*.md", "docs/deep/a.md")
    refute m?("**/*.md", "docs/a.txt")
  end

  test "a trailing /** matches the files under the directory, not the directory" do
    assert m?("docs/**", "docs/a.txt")
    assert m?("docs/**", "docs/a/b.txt")
    refute m?("docs/**", "docs")
    refute m?("docs/**", "other/docs/a.txt")
  end

  test "a directory prefix is a whole path segment" do
    assert m?("android/**", "android/app/build.gradle")
    refute m?("android/**", "androidx/a")
  end

  test "** with no slash crosses directories" do
    assert m?("**.js", "a.js")
    assert m?("**.js", "a/b/c.js")
    assert m?("**", "anything/at/all")
  end

  # `?` used to be "any one character" (`a?c` matched `abc`); on GitHub it
  # makes the preceding character optional.
  test "? is zero or one of the preceding character" do
    assert m?("colou?r", "color")
    assert m?("colou?r", "colour")
    refute m?("colou?r", "colouur")
    assert m?("a?c", "ac")
    assert m?("a?c", "c")
    refute m?("a?c", "abc")
  end

  test "+ is one or more of the preceding character" do
    assert m?("v1+", "v1")
    assert m?("v1+", "v11")
    refute m?("v1+", "v")
    assert m?("a+b", "aaab")
    refute m?("a+b", "b")
  end

  test "quantifiers apply to a class" do
    assert m?("v[0-9]+.[0-9]+", "v10.233")
    refute m?("v[0-9]+.[0-9]+", "v.1")
    assert m?("[ab]?c", "c")
  end

  test "a quantifier applies to a whole multibyte character" do
    assert m?("café+", "cafééé")
    refute m?("café+", "café\xA9")
  end

  test "a ? or + with nothing to quantify is literal" do
    assert m?("?a", "?a")
    refute m?("?a", "a")
    assert m?("+a", "+a")
    assert m?("*?", "x?")
    refute m?("*?", "x")
    assert m?("a++", "aa+")
    refute m?("a++", "aaa")
  end

  test "character classes and ranges" do
    assert m?("[ab].txt", "a.txt")
    assert m?("[ab].txt", "b.txt")
    refute m?("[ab].txt", "c.txt")
    assert m?("v[0-9]", "v7")
    refute m?("v[0-9]", "vx")
    assert m?("[CB]at", "Cat")
    assert m?("[CB]at", "Bat")
    refute m?("[CB]at", "cat")
    assert m?("[a-zA-Z0-9]", "Q")
  end

  test "a bracket the cheat sheet doesn't allow is literal" do
    assert m?("a[b", "a[b")
    assert m?("[!a]", "[!a]")
    refute m?("[!a]", "b")
    assert m?("[^a]", "[^a]")
    assert m?("[.]", "[.]")
    assert m?("[]", "[]")
    assert m?("[a-Z]", "[a-Z]")
  end

  test "a backslash escapes a special character" do
    assert m?("a\\*b", "a*b")
    refute m?("a\\*b", "axb")
    assert m?("\\[x]", "[x]")
    refute m?("\\[x]", "x")
    assert m?("\\!a", "!a")
    assert m?("a\\?", "a?")
    refute m?("a\\?", "a")
  end

  test "dots and other regex characters are literal" do
    assert m?("mix.exs", "mix.exs")
    refute m?("mix.exs", "mixxexs")
    assert m?("a(b|c).txt", "a(b|c).txt")
    refute m?("a(b|c).txt", "ab.txt")
  end

  test "patterns are anchored at both ends" do
    refute m?("lib", "lib/a.ex")
    refute m?("a.ex", "lib/a.ex")
  end

  test "a compiled pattern can be reused" do
    regex = Glob.compile("lib/**")
    assert Glob.match?(regex, "lib/a.ex")
    refute Glob.match?(regex, "test/a.exs")
  end

  describe "select?/2" do
    test "the last matching pattern decides" do
      assert Glob.select?(["releases/**", "!releases/**-alpha"], "releases/1.0")
      refute Glob.select?(["releases/**", "!releases/**-alpha"], "releases/1.0-alpha")
      assert Glob.select?(["*", "!b*", "beta"], "beta")
      refute Glob.select?(["*", "!b*", "beta"], "bravo")
    end

    test "nothing matching selects nothing" do
      refute Glob.select?(["main"], "topic")
      refute Glob.select?([], "topic")
    end

    @tag :capture_log
    test "a list of only negative patterns selects nothing" do
      refute Glob.select?(["!main"], "topic")
      refute Glob.select?(["!main"], "main")
    end

    test "an escaped ! is a positive, literal pattern" do
      assert Glob.select?(["\\!x"], "!x")
    end

    test "takes a compiled list" do
      rules = Glob.compile_list(["src/**", "!src/gen/**"])
      assert Glob.select?(rules, "src/a")
      refute Glob.select?(rules, "src/gen/a")
    end
  end
end