Skip to content

Commit

Permalink
Added lazy evaluation of real directories (pytest-dev#185)
Browse files Browse the repository at this point in the history
* Added lazy evaluation of real directories

- real directory contents are only added to the fake file system on
demand (makes adding large directory trees faster)
- refactored real file/directory access to use own classes

* Added possibility to switch off lazy directory reading

- may be needed for tests that check the disk usage to avoid the side effect of changing disk usage during delayed directory content access
  • Loading branch information
mrbean-bremen authored and jmcgeheeiv committed May 23, 2017
1 parent bbcdb68 commit de0d107
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 36 deletions.
29 changes: 29 additions & 0 deletions fake_filesystem_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4623,6 +4623,35 @@ def testAddExistingRealDirectoryTree(self):
self.assertTrue(self.filesystem.Exists(os.path.join(real_dir_path, 'pyfakefs', 'fake_filesystem.py')))
self.assertTrue(self.filesystem.Exists(os.path.join(real_dir_path, 'pyfakefs', '__init__.py')))

def testGetObjectFromLazilyAddedRealDirectory(self):
self.filesystem.is_case_sensitive = True
real_dir_path = os.path.dirname(__file__)
self.filesystem.add_real_directory(real_dir_path)
self.assertTrue(self.filesystem.GetObject(os.path.join(real_dir_path, 'pyfakefs', 'fake_filesystem.py')))
self.assertTrue(self.filesystem.GetObject(os.path.join(real_dir_path, 'pyfakefs', '__init__.py')))

def testAddExistingRealDirectoryLazily(self):
disk_size = 1024*1024*1024
real_dir_path = os.path.join(os.path.dirname(__file__), 'pyfakefs')
self.filesystem.SetDiskUsage(disk_size, real_dir_path)
self.filesystem.add_real_directory(real_dir_path)

# the directory contents have not been read, the the disk usage has not changed
self.assertEqual(disk_size, self.filesystem.GetDiskUsage(real_dir_path).free)
# checking for existence shall read the directory contents
self.assertTrue(self.filesystem.GetObject(os.path.join(real_dir_path, 'fake_filesystem.py')))
# so now the free disk space shall have decreased
self.assertGreater(disk_size, self.filesystem.GetDiskUsage(real_dir_path).free)

def testAddExistingRealDirectoryNotLazily(self):
disk_size = 1024*1024*1024
real_dir_path = os.path.join(os.path.dirname(__file__), 'pyfakefs')
self.filesystem.SetDiskUsage(disk_size, real_dir_path)
self.filesystem.add_real_directory(real_dir_path, lazy_read=False)

# the directory has been read, so the file sizes have been subtracted from the free space
self.assertGreater(disk_size, self.filesystem.GetDiskUsage(real_dir_path).free)

def testAddExistingRealDirectoryReadWrite(self):
real_dir_path = os.path.join(os.path.dirname(__file__), 'pyfakefs')
self.filesystem.add_real_directory(real_dir_path, read_only=False)
Expand Down
174 changes: 138 additions & 36 deletions pyfakefs/fake_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,8 @@ def __init__(self, name, st_mode=stat.S_IFREG | PERM_DEF_FILE,
self.st_uid = None
self.st_gid = None

# members changed only by _CreateFile() to implement add_real_file()
self.read_from_real_fs = False
self.file_path = None

@property
def byte_contents(self):
if self._byte_contents is None and self.read_from_real_fs:
with io.open(self.file_path, 'rb') as f:
self._byte_contents = f.read()
return self._byte_contents

@property
Expand Down Expand Up @@ -297,7 +290,7 @@ def SetLargeFileSize(self, st_size):

def IsLargeFile(self):
"""Return True if this file was initialized with size but no contents."""
return self._byte_contents is None and not self.read_from_real_fs
return self._byte_contents is None

def _encode_contents(self, contents):
# pylint: disable=undefined-variable
Expand Down Expand Up @@ -425,6 +418,53 @@ def SetIno(self, st_ino):
self.st_ino = st_ino


class FakeFileFromRealFile(FakeFile):
"""Represents a fake file copied from the real file system.
The contents of the file are read on demand only.
New in pyfakefs 3.2.
"""

def __init__(self, file_path, filesystem, read_only=True):
"""init.
Args:
file_path: path to the existing file.
filesystem: the fake filesystem where the file is created.
read_only: if set, the file is treated as read-only, e.g. a write access raises an exception;
otherwise, writing to the file changes the fake file only as usually.
Raises:
OSError: if the file does not exist in the real file system.
"""
real_stat = os.stat(file_path)
# for read-only mode, remove the write/executable permission bits
mode = real_stat.st_mode & 0o777444 if read_only else real_stat.st_mode
super(FakeFileFromRealFile, self).__init__(name=os.path.basename(file_path),
st_mode=mode,
filesystem=filesystem)
self.st_ctime = real_stat.st_ctime
self.st_atime = real_stat.st_atime
self.st_mtime = real_stat.st_mtime
self.st_gid = real_stat.st_gid
self.st_uid = real_stat.st_uid
self.st_size = real_stat.st_size
self.file_path = file_path
self.contents_read = False

@property
def byte_contents(self):
if not self.contents_read:
self.contents_read = True
with io.open(self.file_path, 'rb') as f:
self._byte_contents = f.read()
return self._byte_contents

def IsLargeFile(self):
"""The contents are never faked."""
return False


class FakeDirectory(FakeFile):
"""Provides the appearance of a real directory."""

Expand Down Expand Up @@ -520,6 +560,58 @@ def __str__(self):
return description


class FakeDirectoryFromRealDirectory(FakeDirectory):
"""Represents a fake directory copied from the real file system.
The contents of the directory are read on demand only.
New in pyfakefs 3.2.
"""

def __init__(self, dir_path, filesystem, read_only):
"""init.
Args:
dir_path: full directory path
filesystem: the fake filesystem where the directory is created
read_only: if set, all files under the directory are treated as read-only,
e.g. a write access raises an exception;
otherwise, writing to the files changes the fake files only as usually.
Raises:
OSError if the directory does not exist in the real file system
"""
real_stat = os.stat(dir_path)
super(FakeDirectoryFromRealDirectory, self).__init__(
name=os.path.split(dir_path)[1],
perm_bits=real_stat.st_mode,
filesystem=filesystem)

self.st_ctime = real_stat.st_ctime
self.st_atime = real_stat.st_atime
self.st_mtime = real_stat.st_mtime
self.st_gid = real_stat.st_gid
self.st_uid = real_stat.st_uid
self.dir_path = dir_path
self.read_only = read_only
self.contents_read = False

@property
def contents(self):
"""Return the list of contained directory entries, loading them if not already loaded."""
if not self.contents_read:
self.contents_read = True
self.filesystem.add_real_paths(
[os.path.join(self.dir_path, entry) for entry in os.listdir(self.dir_path)],
read_only=self.read_only)
return self.byte_contents

def GetSize(self):
# we cannot get the size until the contents are loaded
if not self.contents_read:
return 0
return super(FakeDirectoryFromRealDirectory, self).GetSize()


class FakeFilesystem(object):
"""Provides the appearance of a real directory tree for unit testing."""

Expand Down Expand Up @@ -1167,6 +1259,7 @@ def _DirectoryContent(self, directory, component):
if subdir.lower() == component.lower()]
if matching_content:
return matching_content[0]

return None, None

def Exists(self, file_path):
Expand Down Expand Up @@ -1642,13 +1735,11 @@ def add_real_file(self, file_path, read_only=True):
OSError: if the file does not exist in the real file system.
IOError: if the file already exists in the fake file system.
"""
real_stat = os.stat(file_path)
# for read-only mode, remove the write/executable permission bits
mode = real_stat.st_mode & 0o777444 if read_only else real_stat.st_mode
return self._CreateFile(file_path, contents=None, read_from_real_fs=True,
st_mode=mode, real_stat=real_stat)
return self._CreateFile(file_path,
read_from_real_fs=True,
read_only=read_only)

def add_real_directory(self, dir_path, read_only=True):
def add_real_directory(self, dir_path, read_only=True, lazy_read=True):
"""Create fake directory for the existing directory at path, and entries for all contained
files in the real file system.
New in pyfakefs 3.2.
Expand All @@ -1658,6 +1749,11 @@ def add_real_directory(self, dir_path, read_only=True):
read_only: if set, all files under the directory are treated as read-only,
e.g. a write access raises an exception;
otherwise, writing to the files changes the fake files only as usually.
lazy_read: if set (default), directory contents are only read when accessed,
and only until the needed subdirectory level
Note: this means that the file system size is only updated at the time
the directory contents are read; set this to False only if you
are dependent on accurate file system size in your test
Returns:
the newly created FakeDirectory object.
Expand All @@ -1668,12 +1764,24 @@ def add_real_directory(self, dir_path, read_only=True):
"""
if not os.path.exists(dir_path):
raise IOError(errno.ENOENT, 'No such directory', dir_path)
self.CreateDirectory(dir_path)
for base, _, files in os.walk(dir_path):
for fileEntry in files:
self.add_real_file(os.path.join(base, fileEntry), read_only)
if lazy_read:
parent_path = os.path.split(dir_path)[0]
if self.Exists(parent_path):
parent_dir = self.GetObject(parent_path)
else:
parent_dir = self.CreateDirectory(parent_path)
new_dir = FakeDirectoryFromRealDirectory(dir_path, filesystem=self, read_only=read_only)
parent_dir.AddEntry(new_dir)
self.last_ino += 1
new_dir.SetIno(self.last_ino)
else:
new_dir = self.CreateDirectory(dir_path)
for base, _, files in os.walk(dir_path):
for fileEntry in files:
self.add_real_file(os.path.join(base, fileEntry), read_only)
return new_dir

def add_real_paths(self, path_list, read_only=True):
def add_real_paths(self, path_list, read_only=True, lazy_dir_read=True):
"""Convenience method to add several files and directories from the real file system
in the fake file system. See `add_real_file()` and `add_real_directory()`.
New in pyfakefs 3.2.
Expand All @@ -1683,22 +1791,24 @@ def add_real_paths(self, path_list, read_only=True):
read_only: if set, all files and files under under the directories are treated as read-only,
e.g. a write access raises an exception;
otherwise, writing to the files changes the fake files only as usually.
lazy_dir_read: uses lazy reading of directory contents if set
(see `add_real_directory`)
Raises:
OSError: if any of the files and directories in the list does not exist in the real file system.
OSError: if any of the files and directories in the list already exists in the fake file system.
"""
for path in path_list:
if os.path.isdir(path):
self.add_real_directory(path, read_only)
self.add_real_directory(path, read_only, lazy_dir_read)
else:
self.add_real_file(path, read_only)

def _CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE,
contents='', st_size=None, create_missing_dirs=True,
apply_umask=False, encoding=None, errors=None,
read_from_real_fs=False, real_stat=None):
"""Create file_path, including all the parent directories along the way.
read_from_real_fs=False, read_only=True):
"""Internal fake file creation, supports both normal fake files and fake files from real files.
Args:
file_path: path to the file to create.
Expand All @@ -1708,12 +1818,10 @@ def _CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE,
create_missing_dirs: if True, auto create missing directories.
apply_umask: whether or not the current umask must be applied on st_mode.
encoding: if contents is a unicode string, the encoding used for serialization.
New in pyfakefs 2.9.
errors: the error mode used for encoding/decoding errors
New in pyfakefs 3.2.
read_from_real_fs: if True, the contents are reaf from the real file system on demand.
New in pyfakefs 3.2.
real_stat: used in combination with read_from_real_fs; stat result of the real file
read_only: if set, the file is treated as read-only, e.g. a write access raises an exception;
otherwise, writing to the file changes the fake file only as usually.
"""
file_path = self.NormalizePath(file_path)
if self.Exists(file_path):
Expand All @@ -1732,22 +1840,16 @@ def _CreateFile(self, file_path, st_mode=stat.S_IFREG | PERM_DEF_FILE,
parent_directory = self.NormalizeCase(parent_directory)
if apply_umask:
st_mode &= ~self.umask
file_object = FakeFile(new_file, st_mode, filesystem=self, encoding=encoding, errors=errors)
if read_from_real_fs:
file_object.st_ctime = real_stat.st_ctime
file_object.st_atime = real_stat.st_atime
file_object.st_mtime = real_stat.st_mtime
file_object.st_gid = real_stat.st_gid
file_object.st_uid = real_stat.st_uid
file_object.st_size = real_stat.st_size
file_object.read_from_real_fs = True
file_object.file_path = file_path
file_object = FakeFileFromRealFile(file_path, filesystem=self, read_only=read_only)
else:
file_object = FakeFile(new_file, st_mode, filesystem=self, encoding=encoding, errors=errors)

self.last_ino += 1
file_object.SetIno(self.last_ino)
self.AddObject(parent_directory, file_object)

if contents is not None or st_size is not None:
if not read_from_real_fs and (contents is not None or st_size is not None):
try:
if st_size is not None:
file_object.SetLargeFileSize(st_size)
Expand Down

0 comments on commit de0d107

Please sign in to comment.