Files
ios/Scripts/fix-localizable-strings/tests/test_delete_duplicate_strings.py

369 lines
12 KiB
Python

"""Tests for the delete_duplicate_strings module."""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from delete_duplicate_strings import deduplicate, delete_duplicates
class TestDeduplicateNoDuplicates(unittest.TestCase):
"""Cases where the file has no duplicates and should be unchanged."""
def test_empty_file_is_unchanged(self):
content = ""
result, removed = deduplicate(content)
self.assertEqual(result, content)
self.assertEqual(removed, [])
def test_single_entry_is_unchanged(self):
content = '"greeting" = "Hello";\n'
result, removed = deduplicate(content)
self.assertEqual(result, content)
self.assertEqual(removed, [])
def test_multiple_unique_entries_are_unchanged(self):
content = (
'"greeting" = "Hello";\n'
'"farewell" = "Goodbye";\n'
'"thanks" = "Thank you";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, content)
self.assertEqual(removed, [])
def test_comment_only_file_is_unchanged(self):
content = "/* This file is intentionally left blank. */\n"
result, removed = deduplicate(content)
self.assertEqual(result, content)
self.assertEqual(removed, [])
def test_file_with_only_blank_lines_is_unchanged(self):
content = "\n\n\n"
result, removed = deduplicate(content)
self.assertEqual(result, content)
self.assertEqual(removed, [])
class TestDeduplicateEntryValueContainingCommentSyntax(unittest.TestCase):
"""Entries whose values contain comment-like syntax must not be misclassified
as comments, which would suppress deduplication of subsequent entries."""
def test_duplicate_after_entry_with_block_comment_syntax_in_value(self):
content = (
'"key" = "Use /* to start a block comment";\n'
'"key" = "duplicate";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'"key" = "Use /* to start a block comment";\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["key"])
def test_closing_comment_syntax_in_later_value_does_not_corrupt_output(self):
content = (
'"key" = "Use /* to start a block comment";\n'
'"key" = "duplicate";\n'
'"other" = "This */ ends nothing";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'"key" = "Use /* to start a block comment";\n'
'"other" = "This */ ends nothing";\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["key"])
class TestDeduplicateRemovesDuplicates(unittest.TestCase):
"""Cases where duplicates are removed."""
def test_removes_all_occurrences_beyond_first(self):
content = (
'"key" = "first";\n'
'"key" = "second";\n'
'"key" = "third";\n'
)
expected = '"key" = "first";\n'
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["key", "key"])
def test_removes_multiple_distinct_duplicate_keys(self):
content = (
'"alpha" = "A";\n'
'"beta" = "B";\n'
'"alpha" = "A again";\n'
'"beta" = "B again";\n'
)
expected = (
'"alpha" = "A";\n'
'"beta" = "B";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["alpha", "beta"])
def test_non_duplicate_entries_are_preserved(self):
content = (
'"alpha" = "A";\n'
'"beta" = "B";\n'
'"alpha" = "A again";\n'
'"gamma" = "G";\n'
)
expected = (
'"alpha" = "A";\n'
'"beta" = "B";\n'
'"gamma" = "G";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["alpha"])
class TestDeduplicatePreservesFirstOccurrencePosition(unittest.TestCase):
"""The first occurrence stays exactly where it is in the file."""
def test_first_occurrence_remains_at_original_position(self):
content = (
'"other" = "Other";\n'
'"key" = "first";\n'
'"more" = "More";\n'
'"key" = "second";\n'
)
expected = (
'"other" = "Other";\n'
'"key" = "first";\n'
'"more" = "More";\n'
)
result, _ = deduplicate(content)
self.assertEqual(result, expected)
class TestDeduplicateCommentHandling(unittest.TestCase):
"""Comment blocks are removed together with their duplicate entry."""
def test_removes_single_line_block_comment_with_duplicate(self):
content = (
'"greeting" = "Hello";\n'
'/* This is a duplicate */\n'
'"greeting" = "Hi";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'"greeting" = "Hello";\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["greeting"])
def test_removes_line_comment_with_duplicate(self):
content = (
'"greeting" = "Hello";\n'
'// duplicate greeting\n'
'"greeting" = "Hi";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'"greeting" = "Hello";\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["greeting"])
def test_removes_multi_line_block_comment_with_duplicate(self):
content = (
'"greeting" = "Hello";\n'
'/* This comment\n'
' spans multiple lines */\n'
'"greeting" = "Hi";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'"greeting" = "Hello";\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["greeting"])
def test_removes_stacked_comments_with_duplicate(self):
content = (
'"greeting" = "Hello";\n'
'/* Comment one */\n'
'// Comment two\n'
'"greeting" = "Hi";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'"greeting" = "Hello";\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["greeting"])
def test_preserves_comment_on_first_occurrence(self):
content = (
'/* This comment belongs to the first occurrence */\n'
'"greeting" = "Hello";\n'
'"greeting" = "Hi";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'/* This comment belongs to the first occurrence */\n'
'"greeting" = "Hello";\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["greeting"])
def test_preserves_comment_on_non_duplicate_entry(self):
content = (
'"greeting" = "Hello";\n'
'"greeting" = "Hi";\n'
'/* This belongs to farewell */\n'
'"farewell" = "Goodbye";\n'
'"done" = "Done";\n'
)
expected = (
'"greeting" = "Hello";\n'
'/* This belongs to farewell */\n'
'"farewell" = "Goodbye";\n'
'"done" = "Done";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["greeting"])
class TestDeduplicateBlankLineBreaksCommentAssociation(unittest.TestCase):
"""A blank line between a comment and an entry breaks their association.
The comment is preserved even if the entry is a duplicate."""
def test_blank_line_between_comment_and_duplicate_preserves_comment(self):
content = (
'"greeting" = "Hello";\n'
'/* Orphaned comment */\n'
'\n'
'"greeting" = "Hi";\n'
'"farewell" = "Goodbye";\n'
)
expected = (
'"greeting" = "Hello";\n'
'/* Orphaned comment */\n'
'\n'
'"farewell" = "Goodbye";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["greeting"])
def test_blank_lines_between_entries_are_preserved(self):
content = (
'"alpha" = "A";\n'
'\n'
'"beta" = "B";\n'
'\n'
'"gamma" = "G";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, content)
self.assertEqual(removed, [])
def test_blank_lines_in_output_are_preserved_around_removed_entry(self):
content = (
'"alpha" = "A";\n'
'\n'
'"alpha" = "A again";\n'
'\n'
'"beta" = "B";\n'
)
expected = (
'"alpha" = "A";\n'
'\n'
'\n'
'"beta" = "B";\n'
)
result, removed = deduplicate(content)
self.assertEqual(result, expected)
self.assertEqual(removed, ["alpha"])
class TestDeduplicateReturnedRemovedKeys(unittest.TestCase):
"""Verify the removed keys list is correct."""
def test_removed_list_is_empty_when_no_duplicates(self):
content = '"a" = "A";\n"b" = "B";\n'
_, removed = deduplicate(content)
self.assertEqual(removed, [])
def test_removed_list_contains_each_removal_separately(self):
# "a" appears three times, so it should appear twice in removed
content = '"a" = "1";\n"a" = "2";\n"a" = "3";\n'
_, removed = deduplicate(content)
self.assertEqual(removed, ["a", "a"])
def test_removed_list_is_in_encounter_order(self):
content = (
'"x" = "1";\n'
'"y" = "1";\n'
'"x" = "2";\n'
'"y" = "2";\n'
)
_, removed = deduplicate(content)
self.assertEqual(removed, ["x", "y"])
class TestDeleteDuplicatesFileIO(unittest.TestCase):
"""Integration tests for the file I/O wrapper."""
def _write(self, content: str) -> str:
f = tempfile.NamedTemporaryFile(
mode="w", suffix=".strings", delete=False, encoding="utf-8"
)
f.write(content)
f.close()
self.addCleanup(os.unlink, f.name)
return f.name
def test_modifies_file_in_place(self):
path = self._write('"a" = "A";\n"a" = "A again";\n')
delete_duplicates(path)
with open(path) as f:
result = f.read()
self.assertNotIn('"a" = "A again";', result)
def test_returns_removed_keys(self):
path = self._write('"a" = "A";\n"a" = "A again";\n')
removed = delete_duplicates(path)
self.assertEqual(removed, ["a"])
def test_does_not_write_file_when_no_duplicates(self):
path = self._write('"a" = "A";\n')
mtime_before = os.path.getmtime(path)
removed = delete_duplicates(path)
mtime_after = os.path.getmtime(path)
self.assertEqual(removed, [])
self.assertEqual(mtime_before, mtime_after)
def test_returns_empty_list_when_no_duplicates(self):
path = self._write('"a" = "A";\n"b" = "B";\n')
removed = delete_duplicates(path)
self.assertEqual(removed, [])
if __name__ == "__main__":
unittest.main()