1# Copyright (c) 2009-2012 testtools developers. See LICENSE for details.
2
3"""Matchers for things related to the filesystem."""
4
5__all__ = [
6    'FileContains',
7    'DirExists',
8    'FileExists',
9    'HasPermissions',
10    'PathExists',
11    'SamePath',
12    'TarballContains',
13    ]
14
15import os
16import tarfile
17
18from ._basic import Equals
19from ._higherorder import (
20    MatchesAll,
21    MatchesPredicate,
22    )
23from ._impl import (
24    Matcher,
25    )
26
27
28def PathExists():
29    """Matches if the given path exists.
30
31    Use like this::
32
33      assertThat('/some/path', PathExists())
34    """
35    return MatchesPredicate(os.path.exists, "%s does not exist.")
36
37
38def DirExists():
39    """Matches if the path exists and is a directory."""
40    return MatchesAll(
41        PathExists(),
42        MatchesPredicate(os.path.isdir, "%s is not a directory."),
43        first_only=True)
44
45
46def FileExists():
47    """Matches if the given path exists and is a file."""
48    return MatchesAll(
49        PathExists(),
50        MatchesPredicate(os.path.isfile, "%s is not a file."),
51        first_only=True)
52
53
54class DirContains(Matcher):
55    """Matches if the given directory contains files with the given names.
56
57    That is, is the directory listing exactly equal to the given files?
58    """
59
60    def __init__(self, filenames=None, matcher=None):
61        """Construct a ``DirContains`` matcher.
62
63        Can be used in a basic mode where the whole directory listing is
64        matched against an expected directory listing (by passing
65        ``filenames``).  Can also be used in a more advanced way where the
66        whole directory listing is matched against an arbitrary matcher (by
67        passing ``matcher`` instead).
68
69        :param filenames: If specified, match the sorted directory listing
70            against this list of filenames, sorted.
71        :param matcher: If specified, match the sorted directory listing
72            against this matcher.
73        """
74        if filenames == matcher == None:
75            raise AssertionError(
76                "Must provide one of `filenames` or `matcher`.")
77        if None not in (filenames, matcher):
78            raise AssertionError(
79                "Must provide either `filenames` or `matcher`, not both.")
80        if filenames is None:
81            self.matcher = matcher
82        else:
83            self.matcher = Equals(sorted(filenames))
84
85    def match(self, path):
86        mismatch = DirExists().match(path)
87        if mismatch is not None:
88            return mismatch
89        return self.matcher.match(sorted(os.listdir(path)))
90
91
92class FileContains(Matcher):
93    """Matches if the given file has the specified contents."""
94
95    def __init__(self, contents=None, matcher=None):
96        """Construct a ``FileContains`` matcher.
97
98        Can be used in a basic mode where the file contents are compared for
99        equality against the expected file contents (by passing ``contents``).
100        Can also be used in a more advanced way where the file contents are
101        matched against an arbitrary matcher (by passing ``matcher`` instead).
102
103        :param contents: If specified, match the contents of the file with
104            these contents.
105        :param matcher: If specified, match the contents of the file against
106            this matcher.
107        """
108        if contents == matcher == None:
109            raise AssertionError(
110                "Must provide one of `contents` or `matcher`.")
111        if None not in (contents, matcher):
112            raise AssertionError(
113                "Must provide either `contents` or `matcher`, not both.")
114        if matcher is None:
115            self.matcher = Equals(contents)
116        else:
117            self.matcher = matcher
118
119    def match(self, path):
120        mismatch = PathExists().match(path)
121        if mismatch is not None:
122            return mismatch
123        f = open(path)
124        try:
125            actual_contents = f.read()
126            return self.matcher.match(actual_contents)
127        finally:
128            f.close()
129
130    def __str__(self):
131        return "File at path exists and contains %s" % self.contents
132
133
134class HasPermissions(Matcher):
135    """Matches if a file has the given permissions.
136
137    Permissions are specified and matched as a four-digit octal string.
138    """
139
140    def __init__(self, octal_permissions):
141        """Construct a HasPermissions matcher.
142
143        :param octal_permissions: A four digit octal string, representing the
144            intended access permissions. e.g. '0775' for rwxrwxr-x.
145        """
146        super().__init__()
147        self.octal_permissions = octal_permissions
148
149    def match(self, filename):
150        permissions = oct(os.stat(filename).st_mode)[-4:]
151        return Equals(self.octal_permissions).match(permissions)
152
153
154class SamePath(Matcher):
155    """Matches if two paths are the same.
156
157    That is, the paths are equal, or they point to the same file but in
158    different ways.  The paths do not have to exist.
159    """
160
161    def __init__(self, path):
162        super().__init__()
163        self.path = path
164
165    def match(self, other_path):
166        f = lambda x: os.path.abspath(os.path.realpath(x))
167        return Equals(f(self.path)).match(f(other_path))
168
169
170class TarballContains(Matcher):
171    """Matches if the given tarball contains the given paths.
172
173    Uses TarFile.getnames() to get the paths out of the tarball.
174    """
175
176    def __init__(self, paths):
177        super().__init__()
178        self.paths = paths
179        self.path_matcher = Equals(sorted(self.paths))
180
181    def match(self, tarball_path):
182        # Open underlying file first to ensure it's always closed:
183        # <http://bugs.python.org/issue10233>
184        f = open(tarball_path, "rb")
185        try:
186            tarball = tarfile.open(tarball_path, fileobj=f)
187            try:
188                return self.path_matcher.match(sorted(tarball.getnames()))
189            finally:
190                tarball.close()
191        finally:
192            f.close()
193