Skip to content
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6441227
Increase test coverage for filecmp module
Bolotnyj Oct 18, 2026
78aad17
📜🤖 Added by blurb_it.
blurb-it[bot] Oct 18, 2026
f781d4c
Remove unnecessary NEWS entry
Bolotnyj Oct 19, 2026
0062674
Refactor dircmp tests to use public API and clean up resources
Bolotnyj Oct 19, 2026
4644135
Sort imports in test_filecmp per review comment
Bolotnyj Oct 22, 2026
4863951
Apply suggestion from @picnixz
Bolotnyj Oct 22, 2026
553d1c5
Use os_helper.temp_dir() in DirCmpImplementationTests setUp
Bolotnyj Oct 22, 2026
fac6025
Use pathlib.Path for file operations per review comment
Bolotnyj Oct 22, 2026
55a87b8
Apply suggestion from @picnixz
Bolotnyj Oct 22, 2026
2d7d06d
Use support.captured_stdout() and move assertions out of context per …
Bolotnyj Oct 22, 2026
bd65042
Add layout comments to all tests in DirCmpImplementationTests per review
Bolotnyj Oct 22, 2026
c2d6193
Merge branch 'main' into feature/filecmp-coverage
Bolotnyj Oct 22, 2026
4eeb957
Apply suggestion from @picnixz
Bolotnyj Oct 24, 2026
e613f5c
test_filecmp: use pathlib for directory creation in setUp
Bolotnyj Oct 24, 2026
ee8905b
Use pathlib and fix code according to PEP 8 code style
Bolotnyj Oct 24, 2026
f222069
Fix indentation according to PEP 8 code style
Bolotnyj Oct 24, 2026
4469ebd
Apply suggestion from @picnixz
Bolotnyj Oct 28, 2026
f9f7b06
Apply suggestion from @picnixz
Bolotnyj Oct 28, 2026
8f74918
Test coverage for filecmp: use exact filepaths in tests
Bolotnyj Oct 31, 2026
c68621b
Apply suggestion from @picnixz
Bolotnyj Oct 31, 2026
c2e2245
Apply suggestion from @picnixz
Bolotnyj Nov 2, 2026
54dfe05
Remove unused cm variable
Bolotnyj Nov 2, 2026
2046e40
Refactor tests
Bolotnyj Nov 2, 2026
b3825d8
Refactor tests
Bolotnyj Nov 2, 2026
95b5e2a
Remove unused subdirs access
Bolotnyj Nov 2, 2026
5c9f85a
Remove trailing whitespaces
Bolotnyj Nov 2, 2026
b14bd17
Keep with-blocks for patch CMs, trigger lazy properties inside contex…
Bolotnyj Nov 2, 2026
793cc79
Trigger CI lint check
Bolotnyj Nov 2, 2026
3c5f09f
Remove trailing whitespaces in test_filecmp
Bolotnyj Nov 2, 2026
3e6e54f
Apply suggestion from @picnixz
Bolotnyj Nov 2, 2026
fffa266
Add tearDown for cache cleanup
Bolotnyj Nov 2, 2026
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
182 changes: 182 additions & 0 deletions Lib/test/test_filecmp.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import filecmp
import io
import os
import re
import shutil
import stat
import tempfile
import unittest

from pathlib import Path
from test import support
from test.support import os_helper
from unittest.mock import patch


def _create_file_shallow_equal(template_path, new_path):
Expand Down Expand Up @@ -76,6 +80,25 @@ def test_cache_clear(self):
self.assertTrue(len(filecmp._cache) == 0,
"Cache not cleared after calling clear_cache")

def test_cache_overflow_and_hit(self):
filecmp.clear_cache()
Comment thread
Bolotnyj marked this conversation as resolved.
Resolved
self.addCleanup(filecmp.clear_cache)
s1 = filecmp._sig(os.stat(self.name))
s2 = filecmp._sig(os.stat(self.name_same))
base_dir = os.path.dirname(self.name)
for i in range(105):
fake_path2 = os.path.join(base_dir, f"fake_path_{i}")
filecmp._cache[self.name, fake_path2, s1, s2] = True

self.assertGreater(len(filecmp._cache), 100)
res1 = filecmp.cmp(self.name, self.name_same, shallow=False)
self.assertTrue(res1)
self.assertEqual(len(filecmp._cache), 1)

res2 = filecmp.cmp(self.name, self.name_same, shallow=False)
self.assertTrue(res2)


class DirCompareTestCase(unittest.TestCase):
def setUp(self):
tmpdir = tempfile.gettempdir()
Expand Down Expand Up @@ -367,5 +390,164 @@ def _assert_report(self, dircmp_report, expected_report_lines):
self.assertEqual(report_lines, expected_report_lines)


class DirCmpImplementationTests(unittest.TestCase):
def setUp(self):
tmpdir = self.enterContext(os_helper.temp_dir())

self.base_test_dir = os.path.join(tmpdir, 'test_filecmp_working_dir')
os.makedirs(self.base_test_dir, exist_ok=True)

self.dir = os.path.join(self.base_test_dir, 'dir')
self.dir_same = os.path.join(self.base_test_dir, 'dir-same')
self.dir_diff = os.path.join(self.base_test_dir, 'dir-diff')
self.dir_diff_file = os.path.join(self.base_test_dir, 'dir-diff-file')
self.dir_same_shallow = os.path.join(self.base_test_dir, 'dir-same-shallow')

self.dir_ignored = os.path.join(self.dir_same, '.hg')
self.caseinsensitive = os.path.normcase('A') == os.path.normcase('a')
data = 'Contents of file go here.\n'

Path(self.dir).mkdir(parents=True, exist_ok=True)
subdir_path = os.path.join(self.dir, 'subdir')
Path(subdir_path).mkdir(parents=True, exist_ok=True)
dir_file_path = os.path.join(self.dir, "file")
Path(dir_file_path).write_text(data, encoding="utf-8")

for d in (self.dir_same, self.dir_same_shallow,
self.dir_diff, self.dir_diff_file):
Path(d, 'subdir').mkdir(parents=True, exist_ok=True)
fn = 'FiLe' if (self.caseinsensitive and d == self.dir_same) else 'file'
file_path = os.path.join(d, fn)

if d == self.dir_same_shallow:
_create_file_shallow_equal(dir_file_path, file_path)
else:
shutil.copy2(dir_file_path, file_path)

Path(self.dir_diff, 'file2').write_text('An extra file.\n')
Path(self.dir_diff_file, 'file2').write_text('Different contents.\n')

def tearDown(self):
filecmp.clear_cache()

# Test dircmp behavior when os.stat raises OSError on specific files.
# Structure contains 'error_trigger_file.txt' in both directories.
def test_dircmp_os_errors_in_processing(self):
Comment on lines +428 to +435

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Path(self.dir_diff_file, 'file2').write_text('Different contents.\n')
# Test dircmp behavior when os.stat raises OSError on specific files.
# Structure contains 'error_trigger_file.txt' in both directories.
def test_dircmp_os_errors_in_processing(self):
Path(self.dir_diff_file, 'file2').write_text('Different contents.\n')
def tearDown(self):
filecmp.clear_cache()
# Test dircmp behavior when os.stat raises OSError on specific files.
# Structure contains 'error_trigger_file.txt' in both directories.
def test_dircmp_os_errors_in_processing(self):

trigger_file = "error_trigger_file.txt"
for d in (self.dir, self.dir_same):
Path(d, trigger_file).write_text('trigger', encoding='utf-8')

expected_path1 = os.path.normcase(os.path.abspath(os.path.join(self.dir, trigger_file)))
expected_path2 = os.path.normcase(os.path.abspath(os.path.join(self.dir_same, trigger_file)))

original_stat = os.stat

def fake_stat(path, *args, **kwargs):
current_path = os.path.normcase(os.path.abspath(os.fspath(path)))
if current_path == expected_path1 or current_path == expected_path2:
raise OSError
return original_stat(path, *args, **kwargs)

with patch("os.stat", side_effect=fake_stat):
dc = filecmp.dircmp(self.dir, self.dir_same)
dc.common_funny

self.assertIn(trigger_file, dc.common_funny)

# Test common_funny layout with regular files, directories, and sockets.
# 'diff_type.txt' is a directory in one path and a regular file in another.
# 'funny_type.txt' simulates a socket using stat.S_IFSOCK.
def test_dircmp_different_types_and_funny_types(self):
file_diff_type = "diff_type.txt"
file_funny_type = "funny_type.txt"

for d in (self.dir, self.dir_same):
for f in (file_diff_type, file_funny_type):
Path(d, f).write_text("data")

diff_path1 = os.path.normcase(os.path.abspath(os.path.join(self.dir, file_diff_type)))
diff_path2 = os.path.normcase(os.path.abspath(os.path.join(self.dir_same, file_diff_type)))
funny_path1 = os.path.normcase(os.path.abspath(os.path.join(self.dir, file_funny_type)))
funny_path2 = os.path.normcase(os.path.abspath(os.path.join(self.dir_same, file_funny_type)))

original_stat = os.stat

def fake_stat_types(path, *args, **kwargs):
st = original_stat(path, *args, **kwargs)
current_path = os.path.normcase(os.path.abspath(os.fspath(path)))

if current_path == diff_path1 or current_path == diff_path2:
is_dir_same = (current_path == diff_path2)
target_type = stat.S_IFREG if is_dir_same else stat.S_IFDIR
new_mode = (st.st_mode & ~stat.S_IFREG) | target_type
return os.stat_result((new_mode,) + st[1:])

if current_path == funny_path1 or current_path == funny_path2:
new_mode = (st.st_mode & ~stat.S_IFREG) | stat.S_IFSOCK
return os.stat_result((new_mode,) + st[1:])

return st

with patch("os.stat", side_effect=fake_stat_types):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, side_effect is used for raising exceptions. For replacing functions, I believe we can use patch(name, new_callable) or wraps= directly. I never remember exactly what's the exact form though.

dc = filecmp.dircmp(self.dir, self.dir_same)
dc.common_funny

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dc.common_funny

No need for accessing common_funny here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried moving it out, but the test fails. filecmp.dircmp uses lazy properties, so it doesn't actually call os.stat until we access dc.common_funny. We have to keep this access inside the with patch block, otherwise the mock uninstalls too early.


self.assertIn(file_diff_type, dc.common_funny)
self.assertIn(file_funny_type, dc.common_funny)

# Test phase4_closure and report() output for common funny files.
# Modifies os.stat for 'funny_report_file.txt' to return a socket type,
# and patches filecmp._cmp to return an error status (2) for 'funny_file_trigger.txt'.
def test_dircmp_closure_and_report_funnies(self):
deep_dir_1 = os.path.join(self.dir, "subdir", "deep_folder")
deep_dir_2 = os.path.join(self.dir_same, "subdir", "deep_folder")
os.mkdir(deep_dir_1)
os.mkdir(deep_dir_2)

funny_trigger = "funny_report_file.txt"
funny_file_trigger = "funny_file_trigger.txt"

for d in (self.dir, self.dir_same):
for f in (funny_trigger, funny_file_trigger):
Path(d, f).write_text('data', encoding='utf-8')

trigger_path1 = os.path.normcase(os.path.abspath(os.path.join(self.dir, funny_trigger)))
trigger_path2 = os.path.normcase(os.path.abspath(os.path.join(self.dir_same, funny_trigger)))
cmp_path1 = os.path.normcase(os.path.abspath(os.path.join(self.dir, funny_file_trigger)))
cmp_path2 = os.path.normcase(os.path.abspath(os.path.join(self.dir_same, funny_file_trigger)))

original_stat = os.stat
original_cmp = filecmp._cmp

def fake_stat_for_report(path, *args, **kwargs):
current_path = os.path.normcase(os.path.abspath(os.fspath(path)))
if current_path == trigger_path1 or current_path == trigger_path2:
st = original_stat(path, *args, **kwargs)
new_mode = (st.st_mode & ~stat.S_IFREG) | stat.S_IFSOCK
return os.stat_result((new_mode,) + st[1:])
return original_stat(path, *args, **kwargs)

def fake_private_cmp(a, b, sh):
norm_a = os.path.normcase(os.path.abspath(os.fspath(a)))
norm_b = os.path.normcase(os.path.abspath(os.fspath(b)))
if norm_a in (cmp_path1, cmp_path2) or norm_b in (cmp_path1, cmp_path2):
return 2
return original_cmp(a, b, sh)

with (
patch("os.stat", side_effect=fake_stat_for_report),
patch("filecmp._cmp", side_effect=fake_private_cmp),
):
dc = filecmp.dircmp(self.dir, self.dir_same)
Comment thread
Bolotnyj marked this conversation as resolved.
Resolved
dc.phase4_closure()

with support.captured_stdout() as mock_stdout:
dc.report()

output = mock_stdout.getvalue()
self.assertIn("Common funny cases", output)
self.assertIn("Trouble with common files", output)


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