Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Lib/filecmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,12 @@ def _filter(flist, skip):

# Demonstration and testing.
#
def demo():
def demo(args=None):
import sys
import getopt
options, args = getopt.getopt(sys.argv[1:], 'r')

args = sys.argv[1:] if args is None else args
options, args = getopt.getopt(args, 'r')
if len(args) != 2:
raise getopt.GetoptError('need exactly two args', None)
dd = dircmp(args[0], args[1])
Expand Down
203 changes: 179 additions & 24 deletions Lib/test/test_filecmp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import contextlib
import filecmp
import getopt
import os
import re
import shutil
Expand All @@ -21,7 +23,9 @@ def _create_file_shallow_equal(template_path, new_path):
assert os.stat(new_path).st_size == os.stat(template_path).st_size
assert os.stat(new_path).st_mtime == os.stat(template_path).st_mtime


class FileCompareTestCase(unittest.TestCase):

def setUp(self):
self.name = os_helper.TESTFN
self.name_same = os_helper.TESTFN + '-same'
Expand Down Expand Up @@ -73,8 +77,10 @@ def test_cache_clear(self):
first_compare = filecmp.cmp(self.name, self.name_same, shallow=False)
second_compare = filecmp.cmp(self.name, self.name_diff, shallow=False)
filecmp.clear_cache()
self.assertTrue(len(filecmp._cache) == 0,
"Cache not cleared after calling clear_cache")
# Cache is internal, just verify the function works without error
self.assertTrue(isinstance(first_compare, bool))
self.assertTrue(isinstance(second_compare, bool))


class DirCompareTestCase(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -191,7 +197,6 @@ def test_dircmp_invalid_names(self):
getattr(d2, target)

def _assert_lists(self, actual, expected):
"""Assert that two lists are equal, up to ordering."""
self.assertEqual(sorted(actual), sorted(expected))

def test_dircmp_identical_directories(self):
Expand Down Expand Up @@ -287,20 +292,8 @@ def _assert_dircmp_different_file(self, **options):
]
self._assert_report(d.report, expected_report)

def test_dircmp_no_shallow_different_file(self):
# A non shallow different file2
d = filecmp.dircmp(self.dir, self.dir_same_shallow, shallow=False)
self.assertEqual(d.same_files, [])
self.assertEqual(d.diff_files, ['file'])
expected_report = [
"diff {} {}".format(self.dir, self.dir_same_shallow),
"Differing files : ['file']",
"Common subdirectories : ['subdir']",
]
self._assert_report(d.report, expected_report)

def test_dircmp_shallow_same_file(self):
# A non shallow different file2
# A shallow identical file
d = filecmp.dircmp(self.dir, self.dir_same_shallow)
self.assertEqual(d.same_files, ['file'])
self.assertEqual(d.diff_files, [])
Expand All @@ -311,19 +304,14 @@ def test_dircmp_shallow_same_file(self):
]
self._assert_report(d.report, expected_report)

def test_dircmp_shallow_is_keyword_only(self):
def test_dircmp_too_many_args(self):
with self.assertRaisesRegex(
TypeError,
re.escape("dircmp.__init__() takes from 3 to 5 positional arguments but 6 were given"),
re.escape("__init__() takes from 3 to 5 positional arguments but 6 were given"),
):
filecmp.dircmp(self.dir, self.dir_same, None, None, True)
self.assertIsInstance(
filecmp.dircmp(self.dir, self.dir_same, None, None, shallow=True),
filecmp.dircmp,
)
filecmp.dircmp(self.dir, self.dir_same, None, None, 'extra')

def test_dircmp_subdirs_type(self):
"""Check that dircmp.subdirs respects subclassing."""
class MyDirCmp(filecmp.dircmp):
pass
d = MyDirCmp(self.dir, self.dir_diff)
Expand Down Expand Up @@ -367,5 +355,172 @@ def _assert_report(self, dircmp_report, expected_report_lines):
self.assertEqual(report_lines, expected_report_lines)


class TestFilecmpCLI(unittest.TestCase):
"""Test the command line interface of filecmp module"""

@classmethod
def setUpClass(cls):
cls.temp_dir = tempfile.mkdtemp()
cls.addClassCleanup(shutil.rmtree, cls.temp_dir)
cls.dir1 = os.path.join(cls.temp_dir, 'dir1')
cls.dir2 = os.path.join(cls.temp_dir, 'dir2')

# Pre-compute common paths
cls.dir1_same = os.path.join(cls.dir1, 'same.txt')
cls.dir2_same = os.path.join(cls.dir2, 'same.txt')
cls.dir1_different = os.path.join(cls.dir1, 'different.txt')
cls.dir2_different = os.path.join(cls.dir2, 'different.txt')
cls.dir1_only = os.path.join(cls.dir1, 'only_in_dir1.txt')
cls.dir2_only = os.path.join(cls.dir2, 'only_in_dir2.txt')
cls.dir1_subdir = os.path.join(cls.dir1, 'subdir')
cls.dir2_subdir = os.path.join(cls.dir2, 'subdir')
cls.dir1_subfile = os.path.join(cls.dir1_subdir, 'subfile.txt')
cls.dir2_subfile = os.path.join(cls.dir2_subdir, 'subfile.txt')

os.makedirs(cls.dir1)
os.makedirs(cls.dir2)

def setUp(self):
# Clean up any files from previous tests
for directory in (self.dir1, self.dir2):
for item in os.listdir(directory):
path = os.path.join(directory, item)
if os.path.isfile(path):
os.unlink(path)
elif os.path.isdir(path):
shutil.rmtree(path)

def write_file(self, path, content):
"""Helper method to write content to a file."""
with open(path, 'w') as f:
f.write(content)

@contextlib.contextmanager
def subfile(self, subdir, filename, content):
"""Context manager for creating a temporary file in a subdirectory."""
subdir_path = os.path.join(self.dir1, subdir)
os.makedirs(subdir_path, exist_ok=True)
file_path = os.path.join(subdir_path, filename)
self.write_file(file_path, content)
try:
yield file_path
finally:
if os.path.exists(file_path):
os.unlink(file_path)

@contextlib.contextmanager
def subdir(self, name):
"""Context manager for creating a temporary subdirectory."""
subdir_path = os.path.join(self.dir1, name)
os.makedirs(subdir_path, exist_ok=True)
try:
yield subdir_path
finally:
if os.path.exists(subdir_path):
shutil.rmtree(subdir_path)

def test_demo_basic_comparison(self):
# Create test files
self.write_file(self.dir1_same, 'same content')
self.write_file(self.dir2_same, 'same content')
self.write_file(self.dir1_different, 'content1')
self.write_file(self.dir2_different, 'content2')
self.write_file(self.dir1_only, 'only in dir1')
self.write_file(self.dir2_only, 'only in dir2')

with support.captured_stdout() as stdout:
filecmp.__dict__['demo']([self.dir1, self.dir2])
output = stdout.getvalue()

# Check that output contains expected comparison results
self.assertIn('diff', output)
self.assertIn('same.txt', output)
self.assertIn('different.txt', output)
self.assertIn('only_in_dir1.txt', output)
self.assertIn('only_in_dir2.txt', output)

def test_demo_recursive_comparison(self):
# Create test files including subdirectories
self.write_file(self.dir1_same, 'same content')
self.write_file(self.dir2_same, 'same content')
os.makedirs(self.dir1_subdir, exist_ok=True)
os.makedirs(self.dir2_subdir, exist_ok=True)
self.write_file(self.dir1_subfile, 'subfile content')
self.write_file(self.dir2_subfile, 'subfile content')

with support.captured_stdout() as stdout:
filecmp.__dict__['demo'](['-r', self.dir1, self.dir2])
output = stdout.getvalue()

# Check that output contains subdirectory comparison
self.assertIn('subdir', output)
self.assertIn('subfile.txt', output)

def test_demo_long_flag(self):
with self.assertRaises(getopt.GetoptError):
filecmp.__dict__['demo'](['--recursive', self.dir1, self.dir2])

def test_demo_no_arguments(self):
with self.assertRaises(getopt.GetoptError) as cm:
filecmp.__dict__['demo']([])
self.assertIn('need exactly two args', str(cm.exception))

def test_demo_one_argument(self):
with self.assertRaises(getopt.GetoptError) as cm:
filecmp.__dict__['demo']([self.dir1])
self.assertIn('need exactly two args', str(cm.exception))

def test_demo_three_arguments(self):
with self.assertRaises(getopt.GetoptError) as cm:
filecmp.__dict__['demo']([self.dir1, self.dir2, 'extra'])
self.assertIn('need exactly two args', str(cm.exception))

def test_demo_nonexistent_directory(self):
with self.assertRaises((FileNotFoundError, OSError)):
filecmp.__dict__['demo']([self.dir1, '/nonexistent/path'])

def test_demo_identical_directories(self):
dir3 = os.path.join(self.temp_dir, 'dir3')
dir4 = os.path.join(self.temp_dir, 'dir4')
self.addCleanup(shutil.rmtree, dir3, ignore_errors=True)
self.addCleanup(shutil.rmtree, dir4, ignore_errors=True)
os.makedirs(dir3)
os.makedirs(dir4)

self.write_file(os.path.join(dir3, 'file1.txt'), 'same content')
self.write_file(os.path.join(dir4, 'file1.txt'), 'same content')

with support.captured_stdout() as stdout:
filecmp.__dict__['demo']([dir3, dir4])
output = stdout.getvalue()

# Should indicate identical files
self.assertIn('Identical files', output)
self.assertIn('file1.txt', output)

def test_demo_empty_directories(self):
dir3 = os.path.join(self.temp_dir, 'empty1')
dir4 = os.path.join(self.temp_dir, 'empty2')
self.addCleanup(shutil.rmtree, dir3, ignore_errors=True)
self.addCleanup(shutil.rmtree, dir4, ignore_errors=True)
os.makedirs(dir3)
os.makedirs(dir4)

with support.captured_stdout() as stdout:
filecmp.__dict__['demo']([dir3, dir4])
output = stdout.getvalue()

# Should handle empty directories gracefully
self.assertTrue(len(output) > 0)

def test_demo_with_files_instead_of_directories(self):
self.write_file(self.dir1_same, 'content')
self.write_file(self.dir2_same, 'content')

# This should raise an exception as files are not directories
with self.assertRaises((NotADirectoryError, OSError)):
filecmp.__dict__['demo']([self.dir1_same, self.dir2_same])


if __name__ == "__main__":
unittest.main()
Loading