# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt

"""Tests for files.py"""

import os
import os.path
from unittest import mock

import pytest

from coverage import env, files
from coverage.exceptions import ConfigError
from coverage.files import (
    FnmatchMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file,
    actual_path, find_python_files, flat_rootname, fnmatches_to_regex,
)
from tests.coveragetest import CoverageTest


class FilesTest(CoverageTest):
    """Tests of coverage.files."""

    def abs_path(self, p):
        """Return the absolute path for `p`."""
        return os.path.join(abs_file(os.getcwd()), os.path.normpath(p))

    def test_simple(self):
        self.make_file("hello.py")
        files.set_relative_directory()
        assert files.relative_filename("hello.py") == "hello.py"
        a = self.abs_path("hello.py")
        assert a != "hello.py"
        assert files.relative_filename(a) == "hello.py"

    def test_peer_directories(self):
        self.make_file("sub/proj1/file1.py")
        self.make_file("sub/proj2/file2.py")
        a1 = self.abs_path("sub/proj1/file1.py")
        a2 = self.abs_path("sub/proj2/file2.py")
        d = os.path.normpath("sub/proj1")
        os.chdir(d)
        files.set_relative_directory()
        assert files.relative_filename(a1) == "file1.py"
        assert files.relative_filename(a2) == a2

    def test_filepath_contains_absolute_prefix_twice(self):
        # https://github.com/nedbat/coveragepy/issues/194
        # Build a path that has two pieces matching the absolute path prefix.
        # Technically, this test doesn't do that on Windows, but drive
        # letters make that impractical to achieve.
        files.set_relative_directory()
        d = abs_file(os.curdir)
        trick = os.path.splitdrive(d)[1].lstrip(os.path.sep)
        rel = os.path.join('sub', trick, 'file1.py')
        assert files.relative_filename(abs_file(rel)) == rel

    def test_canonical_filename_ensure_cache_hit(self):
        self.make_file("sub/proj1/file1.py")
        d = actual_path(self.abs_path("sub/proj1"))
        os.chdir(d)
        files.set_relative_directory()
        canonical_path = files.canonical_filename('sub/proj1/file1.py')
        assert canonical_path == self.abs_path('file1.py')
        # After the filename has been converted, it should be in the cache.
        assert 'sub/proj1/file1.py' in files.CANONICAL_FILENAME_CACHE
        assert files.canonical_filename('sub/proj1/file1.py') == self.abs_path('file1.py')

    @pytest.mark.parametrize(
        ["curdir", "sep"], [
            ("/", "/"),
            ("X:\\", "\\"),
        ]
    )
    def test_relative_dir_for_root(self, curdir, sep):
        with mock.patch.object(files.os, 'curdir', new=curdir):
            with mock.patch.object(files.os, 'sep', new=sep):
                with mock.patch('coverage.files.os.path.normcase', return_value=curdir):
                    files.set_relative_directory()
                    assert files.relative_directory() == curdir


@pytest.mark.parametrize("original, flat", [
    ("abc.py", "abc_py"),
    ("hellothere", "hellothere"),
    ("a/b/c.py", "d_86bbcbe134d28fd2_c_py"),
    ("a/b/defghi.py", "d_86bbcbe134d28fd2_defghi_py"),
    ("/a/b/c.py", "d_bb25e0ada04227c6_c_py"),
    ("/a/b/defghi.py", "d_bb25e0ada04227c6_defghi_py"),
    (r"c:\foo\bar.html", "d_e7c107482373f299_bar_html"),
    (r"d:\foo\bar.html", "d_584a05dcebc67b46_bar_html"),
    ("Montréal/☺/conf.py", "d_c840497a2c647ce0_conf_py"),
    ( # original:
        r"c:\lorem\ipsum\quia\dolor\sit\amet\consectetur\adipisci\velit\sed" +
        r"\quia\non\numquam\eius\modi\tempora\incidunt\ut\labore\et\dolore" +
        r"\magnam\aliquam\quaerat\voluptatem\ut\enim\ad\minima\veniam\quis" +
        r"\nostrum\exercitationem\ullam\corporis\suscipit\laboriosam" +
        r"\Montréal\☺\my_program.py",
        # flat:
        "d_e597dfacb73a23d5_my_program_py"
     ),
])
def test_flat_rootname(original, flat):
    assert flat_rootname(original) == flat


@pytest.mark.parametrize(
        "patterns, case_insensitive, partial," +
            "matches," +
            "nomatches",
[
    (
        ["abc", "xyz"], False, False,
            ["abc", "xyz"],
            ["ABC", "xYz", "abcx", "xabc", "axyz", "xyza"],
    ),
    (
        ["abc", "xyz"], True, False,
            ["abc", "xyz", "Abc", "XYZ", "AbC"],
            ["abcx", "xabc", "axyz", "xyza"],
    ),
    (
        ["abc/hi.py"], True, False,
            ["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"],
            ["abc_hi.py", "abc/hi.pyc"],
    ),
    (
        [r"abc\hi.py"], True, False,
            [r"abc\hi.py", r"ABC\hi.py"],
            ["abc/hi.py", "ABC/hi.py", "abc_hi.py", "abc/hi.pyc"],
    ),
    (
        ["abc/*/hi.py"], True, False,
            ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
            ["abc/hi.py", "abc/hi.pyc"],
    ),
    (
        ["abc/[a-f]*/hi.py"], True, False,
            ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
            ["abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc"],
    ),
    (
        ["abc/"], True, True,
            ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
            ["abcd/foo.py", "xabc/hi.py"],
    ),
])
def test_fnmatches_to_regex(patterns, case_insensitive, partial, matches, nomatches):
    regex = fnmatches_to_regex(patterns, case_insensitive=case_insensitive, partial=partial)
    for s in matches:
        assert regex.match(s)
    for s in nomatches:
        assert not regex.match(s)


class MatcherTest(CoverageTest):
    """Tests of file matchers."""

    def setUp(self):
        super().setUp()
        files.set_relative_directory()

    def assertMatches(self, matcher, filepath, matches):
        """The `matcher` should agree with `matches` about `filepath`."""
        canonical = files.canonical_filename(filepath)
        msg = f"File {filepath} should have matched as {matches}"
        assert matches == matcher.match(canonical), msg

    def test_tree_matcher(self):
        case_folding = env.WINDOWS
        matches_to_try = [
            (self.make_file("sub/file1.py"), True),
            (self.make_file("sub/file2.c"), True),
            (self.make_file("sub2/file3.h"), False),
            (self.make_file("sub3/file4.py"), True),
            (self.make_file("sub3/file5.c"), False),
            (self.make_file("sub4/File5.py"), case_folding),
            (self.make_file("sub5/file6.py"), case_folding),
        ]
        trees = [
            files.canonical_filename("sub"),
            files.canonical_filename("sub3/file4.py"),
            files.canonical_filename("sub4/file5.py"),
            files.canonical_filename("SUB5/file6.py"),
        ]
        tm = TreeMatcher(trees)
        assert tm.info() == sorted(trees)
        for filepath, matches in matches_to_try:
            self.assertMatches(tm, filepath, matches)

    def test_module_matcher(self):
        matches_to_try = [
            ('test', True),
            ('trash', False),
            ('testing', False),
            ('test.x', True),
            ('test.x.y.z', True),
            ('py', False),
            ('py.t', False),
            ('py.test', True),
            ('py.testing', False),
            ('py.test.buz', True),
            ('py.test.buz.baz', True),
            ('__main__', False),
            ('mymain', True),
            ('yourmain', False),
        ]
        modules = ['test', 'py.test', 'mymain']
        mm = ModuleMatcher(modules)
        assert mm.info() == modules
        for modulename, matches in matches_to_try:
            assert mm.match(modulename) == matches, modulename

    def test_fnmatch_matcher(self):
        matches_to_try = [
            (self.make_file("sub/file1.py"), True),
            (self.make_file("sub/file2.c"), False),
            (self.make_file("sub2/file3.h"), True),
            (self.make_file("sub3/file4.py"), True),
            (self.make_file("sub3/file5.c"), False),
        ]
        fnm = FnmatchMatcher(["*.py", "*/sub2/*"])
        assert fnm.info() == ["*.py", "*/sub2/*"]
        for filepath, matches in matches_to_try:
            self.assertMatches(fnm, filepath, matches)

    def test_fnmatch_matcher_overload(self):
        fnm = FnmatchMatcher(["*x%03d*.txt" % i for i in range(500)])
        self.assertMatches(fnm, "x007foo.txt", True)
        self.assertMatches(fnm, "x123foo.txt", True)
        self.assertMatches(fnm, "x798bar.txt", False)

    def test_fnmatch_windows_paths(self):
        # We should be able to match Windows paths even if we are running on
        # a non-Windows OS.
        fnm = FnmatchMatcher(["*/foo.py"])
        self.assertMatches(fnm, r"dir\foo.py", True)
        fnm = FnmatchMatcher([r"*\foo.py"])
        self.assertMatches(fnm, r"dir\foo.py", True)


@pytest.fixture(params=[False, True], name="rel_yn")
def relative_setting(request):
    """Parameterized fixture to choose whether PathAliases is relative or not."""
    return request.param


class PathAliasesTest(CoverageTest):
    """Tests for coverage/files.py:PathAliases"""

    run_in_temp_dir = False

    def assert_mapped(self, aliases, inp, out, relative=False):
        """Assert that `inp` mapped through `aliases` produces `out`.

        `out` is canonicalized first, since aliases produce canonicalized
        paths by default.

        """
        mapped = aliases.map(inp)
        expected = files.canonical_filename(out) if not relative else out
        assert mapped == expected

    def assert_unchanged(self, aliases, inp):
        """Assert that `inp` mapped through `aliases` is unchanged."""
        assert aliases.map(inp) == inp

    def test_noop(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        self.assert_unchanged(aliases, '/ned/home/a.py')

    def test_nomatch(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        aliases.add('/home/*/src', './mysrc')
        self.assert_unchanged(aliases, '/home/foo/a.py')

    def test_wildcard(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        aliases.add('/ned/home/*/src', './mysrc')
        self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)

        aliases = PathAliases(relative=rel_yn)
        aliases.add('/ned/home/*/src/', './mysrc')
        self.assert_mapped(aliases, '/ned/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)

    def test_no_accidental_match(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        aliases.add('/home/*/src', './mysrc')
        self.assert_unchanged(aliases, '/home/foo/srcetc')

    def test_multiple_patterns(self, rel_yn):
        # also test the debugfn...
        msgs = []
        aliases = PathAliases(debugfn=msgs.append, relative=rel_yn)
        aliases.add('/home/*/src', './mysrc')
        aliases.add('/lib/*/libsrc', './mylib')
        self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
        self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py', relative=rel_yn)
        if rel_yn:
            assert msgs == [
                "Aliases (relative=True):",
                " Rule: '/home/*/src' -> './mysrc/' using regex " +
                    "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'",
                " Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
                    "'(?:(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/]))'",
                "Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " +
                    "producing './mysrc/a.py'",
                "Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " +
                    "producing './mylib/a.py'",
            ]
        else:
            assert msgs == [
                "Aliases (relative=False):",
                " Rule: '/home/*/src' -> './mysrc/' using regex " +
                    "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'",
                " Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
                    "'(?:(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/]))'",
                "Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " +
                    f"producing {files.canonical_filename('./mysrc/a.py')!r}",
                "Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " +
                    f"producing {files.canonical_filename('./mylib/a.py')!r}",
            ]

    @pytest.mark.parametrize("badpat", [
        "/ned/home/*",
        "/ned/home/*/",
        "/ned/home/*/*/",
    ])
    def test_cant_have_wildcard_at_end(self, badpat):
        aliases = PathAliases()
        msg = "Pattern must not end with wildcards."
        with pytest.raises(ConfigError, match=msg):
            aliases.add(badpat, "fooey")

    def test_no_accidental_munging(self):
        aliases = PathAliases()
        aliases.add(r'c:\Zoo\boo', 'src/')
        aliases.add('/home/ned$', 'src/')
        self.assert_mapped(aliases, r'c:\Zoo\boo\foo.py', 'src/foo.py')
        self.assert_mapped(aliases, r'/home/ned$/foo.py', 'src/foo.py')

    def test_paths_are_os_corrected(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        aliases.add('/home/ned/*/src', './mysrc')
        aliases.add(r'c:\ned\src', './mysrc')
        self.assert_mapped(aliases, r'C:\Ned\src\sub\a.py', './mysrc/sub/a.py', relative=rel_yn)

        aliases = PathAliases(relative=rel_yn)
        aliases.add('/home/ned/*/src', r'.\mysrc')
        aliases.add(r'c:\ned\src', r'.\mysrc')
        self.assert_mapped(
            aliases,
            r'/home/ned/foo/src/sub/a.py',
            r'.\mysrc\sub\a.py',
            relative=rel_yn,
        )

    def test_windows_on_linux(self, rel_yn):
        # https://github.com/nedbat/coveragepy/issues/618
        lin = "*/project/module/"
        win = "*\\project\\module\\"

        # Try the paths in both orders.
        for paths in [[lin, win], [win, lin]]:
            aliases = PathAliases(relative=rel_yn)
            for path in paths:
                aliases.add(path, "project/module")
            self.assert_mapped(
                aliases,
                "C:\\a\\path\\somewhere\\coveragepy_test\\project\\module\\tests\\file.py",
                "project/module/tests/file.py",
                relative=rel_yn,
            )

    def test_linux_on_windows(self, rel_yn):
        # https://github.com/nedbat/coveragepy/issues/618
        lin = "*/project/module/"
        win = "*\\project\\module\\"

        # Try the paths in both orders.
        for paths in [[lin, win], [win, lin]]:
            aliases = PathAliases(relative=rel_yn)
            for path in paths:
                aliases.add(path, "project\\module")
            self.assert_mapped(
                aliases,
                "C:/a/path/somewhere/coveragepy_test/project/module/tests/file.py",
                "project\\module\\tests\\file.py",
                relative=rel_yn,
            )

    def test_multiple_wildcard(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        aliases.add('/home/jenkins/*/a/*/b/*/django', './django')
        self.assert_mapped(
            aliases,
            '/home/jenkins/xx/a/yy/b/zz/django/foo/bar.py',
            './django/foo/bar.py',
            relative=rel_yn,
        )

    def test_windows_root_paths(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        aliases.add('X:\\', '/tmp/src')
        self.assert_mapped(
            aliases,
            "X:\\a\\file.py",
            "/tmp/src/a/file.py",
            relative=rel_yn,
        )
        self.assert_mapped(
            aliases,
            "X:\\file.py",
            "/tmp/src/file.py",
            relative=rel_yn,
        )

    def test_leading_wildcard(self, rel_yn):
        aliases = PathAliases(relative=rel_yn)
        aliases.add('*/d1', './mysrc1')
        aliases.add('*/d2', './mysrc2')
        self.assert_mapped(aliases, '/foo/bar/d1/x.py', './mysrc1/x.py', relative=rel_yn)
        self.assert_mapped(aliases, '/foo/bar/d2/y.py', './mysrc2/y.py', relative=rel_yn)

    # The root test case was added for the manylinux Docker images,
    # and I'm not sure how it should work on Windows, so skip it.
    cases = [".", "..", "../other"]
    if not env.WINDOWS:
        cases += ["/"]
    @pytest.mark.parametrize("dirname", cases)
    def test_dot(self, dirname):
        aliases = PathAliases()
        aliases.add(dirname, '/the/source')
        the_file = os.path.join(dirname, 'a.py')
        the_file = os.path.expanduser(the_file)
        the_file = os.path.abspath(os.path.realpath(the_file))

        assert '~' not in the_file  # to be sure the test is pure.
        self.assert_mapped(aliases, the_file, '/the/source/a.py')


class FindPythonFilesTest(CoverageTest):
    """Tests of `find_python_files`."""

    def test_find_python_files(self):
        self.make_file("sub/a.py")
        self.make_file("sub/b.py")
        self.make_file("sub/x.c")                   # nope: not .py
        self.make_file("sub/ssub/__init__.py")
        self.make_file("sub/ssub/s.py")
        self.make_file("sub/ssub/~s.py")            # nope: editor effluvia
        self.make_file("sub/lab/exp.py")            # nope: no __init__.py
        self.make_file("sub/windows.pyw")
        py_files = set(find_python_files("sub"))
        self.assert_same_files(py_files, [
            "sub/a.py", "sub/b.py",
            "sub/ssub/__init__.py", "sub/ssub/s.py",
            "sub/windows.pyw",
        ])


@pytest.mark.skipif(not env.WINDOWS, reason="Only need to run Windows tests on Windows.")
class WindowsFileTest(CoverageTest):
    """Windows-specific tests of file name handling."""

    run_in_temp_dir = False

    def test_actual_path(self):
        assert actual_path(r'c:\Windows') == actual_path(r'C:\wINDOWS')
