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