Repository navigation
Increase test coverage for filecmp
#159663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Bolotnyj
wants to merge
16
commits into
python:main
Choose a base branch
from
Bolotnyj:feature/filecmp-coverage
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+160
−0
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
6441227
Increase test coverage for filecmp module
Bolotnyj 78aad17
📜🤖 Added by blurb_it.
blurb-it[bot] f781d4c
Remove unnecessary NEWS entry
Bolotnyj 0062674
Refactor dircmp tests to use public API and clean up resources
Bolotnyj 4644135
Sort imports in test_filecmp per review comment
Bolotnyj 4863951
Apply suggestion from @picnixz
Bolotnyj 553d1c5
Use os_helper.temp_dir() in DirCmpImplementationTests setUp
Bolotnyj fac6025
Use pathlib.Path for file operations per review comment
Bolotnyj 55a87b8
Apply suggestion from @picnixz
Bolotnyj 2d7d06d
Use support.captured_stdout() and move assertions out of context per …
Bolotnyj bd65042
Add layout comments to all tests in DirCmpImplementationTests per review
Bolotnyj c2d6193
Merge branch 'main' into feature/filecmp-coverage
Bolotnyj 4eeb957
Apply suggestion from @picnixz
Bolotnyj e613f5c
test_filecmp: use pathlib for directory creation in setUp
Bolotnyj ee8905b
Use pathlib and fix code according to PEP 8 code style
Bolotnyj f222069
Fix indentation according to PEP 8 code style
Bolotnyj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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): | ||||||||||||
|
|
@@ -76,6 +80,23 @@ 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() | ||||||||||||
| s1 = filecmp._sig(os.stat(self.name)) | ||||||||||||
| s2 = filecmp._sig(os.stat(self.name_same)) | ||||||||||||
| for i in range(105): | ||||||||||||
| fake_path2 = 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() | ||||||||||||
|
|
@@ -367,5 +388,144 @@ def _assert_report(self, dircmp_report, expected_report_lines): | |||||||||||
| self.assertEqual(report_lines, expected_report_lines) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class DirCmpImplementationTests(unittest.TestCase): | ||||||||||||
| def setUp(self): | ||||||||||||
| cm = os_helper.temp_dir() | ||||||||||||
| tmpdir = cm.__enter__() | ||||||||||||
| self.addCleanup(cm.__exit__, None, None, None) | ||||||||||||
|
|
||||||||||||
| 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') | ||||||||||||
|
|
||||||||||||
| # 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') | ||||||||||||
|
|
||||||||||||
| original_stat = os.stat | ||||||||||||
|
|
||||||||||||
| def fake_stat(path, *args, **kwargs): | ||||||||||||
| if trigger_file in str(path): | ||||||||||||
| raise OSError | ||||||||||||
| return original_stat(path, *args, **kwargs) | ||||||||||||
|
|
||||||||||||
| with patch("os.stat", side_effect=fake_stat): | ||||||||||||
| dc = filecmp.dircmp(self.dir, self.dir_same) | ||||||||||||
| 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") | ||||||||||||
|
|
||||||||||||
| original_stat = os.stat | ||||||||||||
|
|
||||||||||||
| def fake_stat_types(path, *args, **kwargs): | ||||||||||||
| st = original_stat(path, *args, **kwargs) | ||||||||||||
| path_str = str(path) | ||||||||||||
|
|
||||||||||||
| if file_diff_type in path_str: | ||||||||||||
| is_dir_same = "dir-same" in path_str | ||||||||||||
| 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 file_funny_type in path_str: | ||||||||||||
| 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): | ||||||||||||
| dc = filecmp.dircmp(self.dir, self.dir_same) | ||||||||||||
| 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') | ||||||||||||
|
|
||||||||||||
| original_stat = os.stat | ||||||||||||
| original_cmp = filecmp._cmp | ||||||||||||
|
|
||||||||||||
| def fake_stat_for_report(path, *args, **kwargs): | ||||||||||||
| if funny_trigger in str(path): | ||||||||||||
| 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): | ||||||||||||
| if funny_file_trigger in str(a) or funny_file_trigger in str(b): | ||||||||||||
| 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 on lines
+516
to
+518
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| _ = dc.subdirs | ||||||||||||
| 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() | ||||||||||||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.