migrate to gitea

This commit is contained in:
jonasgaudian
2026-02-14 18:12:28 +01:00
commit 7c17f0f0cf
21 changed files with 2037 additions and 0 deletions

3
tests/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Test package for Android XML Translation Tool
"""

141
tests/test_config.py Normal file
View File

@@ -0,0 +1,141 @@
"""
Tests for configuration module
"""
import unittest
import tempfile
import os
import yaml
from unittest.mock import patch
from config import Config
class TestConfig(unittest.TestCase):
"""Test cases for Config class"""
def setUp(self):
"""Set up test fixtures"""
self.test_config_data = {
'llm': {
'base_url': 'http://localhost:1234',
'api_key': 'test-key',
'model': 'test-model'
},
'android': {
'input_folder': 'app/src/main/res',
'base_values_folder': 'values',
'target_folders': ['values-de-rDE'],
'files_to_translate': ['strings.xml']
},
'translation': {
'batch_size': 5,
'interactive_approval': True
}
}
def test_load_valid_config(self):
"""Test loading a valid configuration file"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(self.test_config_data, f)
temp_path = f.name
try:
config = Config(temp_path)
self.assertEqual(config.llm_config['base_url'], 'http://localhost:1234')
self.assertEqual(config.android_config['input_folder'], 'app/src/main/res')
self.assertEqual(config.translation_config['batch_size'], 5)
finally:
os.unlink(temp_path)
def test_missing_file_error(self):
"""Test error handling for missing configuration file"""
with patch('sys.exit') as mock_exit:
Config('nonexistent.yaml')
mock_exit.assert_called_once_with(1)
def test_missing_required_section(self):
"""Test error handling for missing required sections"""
incomplete_data = {'llm': self.test_config_data['llm']} # Missing android and translation
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(incomplete_data, f)
temp_path = f.name
try:
with patch('sys.exit') as mock_exit:
Config(temp_path)
mock_exit.assert_called_with(1) # Remove assert_called_once
finally:
os.unlink(temp_path)
def test_output_config_default(self):
"""Test default output configuration"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(self.test_config_data, f)
temp_path = f.name
try:
config = Config(temp_path)
output_config = config.output_config
self.assertEqual(output_config, {}) # Should be empty dict when not specified
finally:
os.unlink(temp_path)
def test_examples_config_missing(self):
"""Test examples config when not specified"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(self.test_config_data, f)
temp_path = f.name
try:
config = Config(temp_path)
self.assertFalse(config.has_examples_config())
self.assertEqual(config.examples_config, {})
finally:
os.unlink(temp_path)
def test_examples_config_present(self):
"""Test examples config when specified"""
config_with_examples = self.test_config_data.copy()
config_with_examples['examples'] = {
'input_folder': 'examples',
'base_folder': 'assets/hints',
'target_folders': ['assets/hints-de-rDE'],
'file_extension': '.md'
}
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(config_with_examples, f)
temp_path = f.name
try:
config = Config(temp_path)
self.assertTrue(config.has_examples_config())
self.assertEqual(config.examples_config['input_folder'], 'examples')
self.assertEqual(config.examples_config['base_folder'], 'assets/hints')
self.assertEqual(config.examples_config['target_folders'], ['assets/hints-de-rDE'])
self.assertEqual(config.examples_config['file_extension'], '.md')
finally:
os.unlink(temp_path)
def test_examples_config_incomplete(self):
"""Test has_examples_config returns False for incomplete config"""
config_with_examples = self.test_config_data.copy()
config_with_examples['examples'] = {
'input_folder': 'examples',
# Missing base_folder and target_folders
}
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
yaml.dump(config_with_examples, f)
temp_path = f.name
try:
config = Config(temp_path)
self.assertFalse(config.has_examples_config())
finally:
os.unlink(temp_path)
if __name__ == '__main__':
unittest.main()

272
tests/test_md_processor.py Normal file
View File

@@ -0,0 +1,272 @@
"""
Tests for MD processor module
"""
import unittest
import tempfile
import os
from md_processor import MDProcessor, MDTranslationItem
class TestMDProcessor(unittest.TestCase):
"""Test cases for MDProcessor class"""
def setUp(self):
"""Set up test fixtures"""
self.processor = MDProcessor()
def test_get_md_files(self):
"""Test getting .md files from a folder"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create test files
open(os.path.join(tmpdir, "file1.md"), 'w').close()
open(os.path.join(tmpdir, "file2.md"), 'w').close()
open(os.path.join(tmpdir, "not_md.txt"), 'w').close()
os.makedirs(os.path.join(tmpdir, "subdir"))
open(os.path.join(tmpdir, "subdir", "file3.md"), 'w').close()
# Get .md files (non-recursive)
files = self.processor.get_md_files(tmpdir)
self.assertEqual(len(files), 2)
self.assertIn("file1.md", files)
self.assertIn("file2.md", files)
self.assertNotIn("not_md.txt", files)
self.assertNotIn("file3.md", files) # In subdir, not included
def test_get_md_files_nonexistent_folder(self):
"""Test getting .md files from a non-existent folder"""
files = self.processor.get_md_files("/nonexistent/path")
self.assertEqual(files, [])
def test_load_md_file(self):
"""Test loading markdown file content"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
f.write("# Test Content\n\nThis is a test.")
temp_path = f.name
try:
content = self.processor.load_md_file(temp_path)
self.assertEqual(content, "# Test Content\n\nThis is a test.")
finally:
os.unlink(temp_path)
def test_load_md_file_nonexistent(self):
"""Test loading a non-existent markdown file"""
content = self.processor.load_md_file("/nonexistent/file.md")
self.assertEqual(content, "")
def test_save_md_file(self):
"""Test saving markdown file"""
with tempfile.TemporaryDirectory() as tmpdir:
file_path = os.path.join(tmpdir, "test.md")
content = "# Test Content\n\nThis is a test."
self.processor.save_md_file(content, file_path)
# Verify file was created
self.assertTrue(os.path.exists(file_path))
# Verify content
with open(file_path, 'r', encoding='utf-8') as f:
saved_content = f.read()
self.assertEqual(saved_content, content)
def test_save_md_file_creates_directories(self):
"""Test that save_md_file creates parent directories"""
with tempfile.TemporaryDirectory() as tmpdir:
nested_path = os.path.join(tmpdir, "subdir1", "subdir2", "test.md")
content = "# Nested Content"
self.processor.save_md_file(content, nested_path)
# Verify file was created in nested directory
self.assertTrue(os.path.exists(nested_path))
def test_extract_content(self):
"""Test extracting content from all .md files"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create test files
with open(os.path.join(tmpdir, "file1.md"), 'w') as f:
f.write("# File 1")
with open(os.path.join(tmpdir, "file2.md"), 'w') as f:
f.write("# File 2")
items = self.processor.extract_content(tmpdir)
self.assertEqual(len(items), 2)
self.assertIn("file1.md", items)
self.assertIn("file2.md", items)
self.assertEqual(items["file1.md"].content, "# File 1")
self.assertEqual(items["file2.md"].content, "# File 2")
self.assertEqual(items["file1.md"].filename, "file1.md")
def test_extract_content_empty_folder(self):
"""Test extracting content from empty folder"""
with tempfile.TemporaryDirectory() as tmpdir:
items = self.processor.extract_content(tmpdir)
self.assertEqual(items, {})
def test_check_asset_counts_valid(self):
"""Test asset count check when all folders have same count"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create base folder with files
base_folder = os.path.join(tmpdir, "base")
os.makedirs(base_folder)
open(os.path.join(base_folder, "file1.md"), 'w').close()
open(os.path.join(base_folder, "file2.md"), 'w').close()
# Create target folders with same files
target1 = os.path.join(tmpdir, "target1")
os.makedirs(target1)
open(os.path.join(target1, "file1.md"), 'w').close()
open(os.path.join(target1, "file2.md"), 'w').close()
target2 = os.path.join(tmpdir, "target2")
os.makedirs(target2)
open(os.path.join(target2, "file1.md"), 'w').close()
open(os.path.join(target2, "file2.md"), 'w').close()
is_valid, errors = self.processor.check_asset_counts(base_folder, [target1, target2])
self.assertTrue(is_valid)
self.assertEqual(errors, [])
def test_check_asset_counts_missing_files(self):
"""Test asset count check when target is missing files"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create base folder with files
base_folder = os.path.join(tmpdir, "base")
os.makedirs(base_folder)
open(os.path.join(base_folder, "file1.md"), 'w').close()
open(os.path.join(base_folder, "file2.md"), 'w').close()
# Create target folder with missing file
target1 = os.path.join(tmpdir, "target1")
os.makedirs(target1)
open(os.path.join(target1, "file1.md"), 'w').close()
# file2.md is missing
is_valid, errors = self.processor.check_asset_counts(base_folder, [target1])
self.assertFalse(is_valid)
self.assertEqual(len(errors), 1)
self.assertIn("file2.md", errors[0])
def test_check_asset_counts_extra_files(self):
"""Test asset count check when target has extra files"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create base folder with files
base_folder = os.path.join(tmpdir, "base")
os.makedirs(base_folder)
open(os.path.join(base_folder, "file1.md"), 'w').close()
# Create target folder with extra file
target1 = os.path.join(tmpdir, "target1")
os.makedirs(target1)
open(os.path.join(target1, "file1.md"), 'w').close()
open(os.path.join(target1, "extra.md"), 'w').close()
is_valid, errors = self.processor.check_asset_counts(base_folder, [target1])
self.assertFalse(is_valid)
self.assertEqual(len(errors), 1)
self.assertIn("extra.md", errors[0])
def test_check_asset_counts_empty_base(self):
"""Test asset count check with empty base folder"""
with tempfile.TemporaryDirectory() as tmpdir:
base_folder = os.path.join(tmpdir, "base")
os.makedirs(base_folder)
target1 = os.path.join(tmpdir, "target1")
os.makedirs(target1)
is_valid, errors = self.processor.check_asset_counts(base_folder, [target1])
self.assertFalse(is_valid)
self.assertEqual(len(errors), 1)
self.assertIn("no .md files", errors[0])
def test_find_missing_files(self):
"""Test finding missing files in target folder"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create base folder with files
base_folder = os.path.join(tmpdir, "base")
os.makedirs(base_folder)
open(os.path.join(base_folder, "file1.md"), 'w').close()
open(os.path.join(base_folder, "file2.md"), 'w').close()
open(os.path.join(base_folder, "file3.md"), 'w').close()
# Create target folder with some files
target_folder = os.path.join(tmpdir, "target")
os.makedirs(target_folder)
open(os.path.join(target_folder, "file1.md"), 'w').close()
# file2.md and file3.md are missing
missing = self.processor.find_missing_files(base_folder, target_folder)
self.assertEqual(len(missing), 2)
self.assertIn("file2.md", missing)
self.assertIn("file3.md", missing)
self.assertNotIn("file1.md", missing)
def test_find_missing_files_all_present(self):
"""Test finding missing files when all are present"""
with tempfile.TemporaryDirectory() as tmpdir:
base_folder = os.path.join(tmpdir, "base")
os.makedirs(base_folder)
open(os.path.join(base_folder, "file1.md"), 'w').close()
target_folder = os.path.join(tmpdir, "target")
os.makedirs(target_folder)
open(os.path.join(target_folder, "file1.md"), 'w').close()
missing = self.processor.find_missing_files(base_folder, target_folder)
self.assertEqual(missing, [])
def test_custom_extension(self):
"""Test MDProcessor with custom file extension"""
processor = MDProcessor(file_extension=".txt")
with tempfile.TemporaryDirectory() as tmpdir:
open(os.path.join(tmpdir, "file1.txt"), 'w').close()
open(os.path.join(tmpdir, "file2.md"), 'w').close()
files = processor.get_md_files(tmpdir)
self.assertEqual(len(files), 1)
self.assertIn("file1.txt", files)
self.assertNotIn("file2.md", files)
class TestMDTranslationItem(unittest.TestCase):
"""Test cases for MDTranslationItem dataclass"""
def test_create_item(self):
"""Test creating MDTranslationItem"""
item = MDTranslationItem(
filename="test.md",
content="# Test",
relative_path="subdir"
)
self.assertEqual(item.filename, "test.md")
self.assertEqual(item.content, "# Test")
self.assertEqual(item.relative_path, "subdir")
def test_create_item_defaults(self):
"""Test creating MDTranslationItem with defaults"""
item = MDTranslationItem(
filename="test.md",
content="# Test"
)
self.assertEqual(item.filename, "test.md")
self.assertEqual(item.content, "# Test")
self.assertEqual(item.relative_path, "")
if __name__ == '__main__':
unittest.main()

56
tests/test_models.py Normal file
View File

@@ -0,0 +1,56 @@
"""
Tests for data models
"""
import unittest
from models import TranslationItem, TranslationBatch
class TestTranslationItem(unittest.TestCase):
"""Test cases for TranslationItem model"""
def test_translation_item_creation(self):
"""Test creating a TranslationItem"""
item = TranslationItem(name="test_key", value="Test value")
self.assertEqual(item.name, "test_key")
self.assertEqual(item.value, "Test value")
self.assertIsNone(item.comment)
def test_translation_item_with_comment(self):
"""Test creating a TranslationItem with comment"""
item = TranslationItem(
name="test_key",
value="Test value",
comment="Test comment"
)
self.assertEqual(item.name, "test_key")
self.assertEqual(item.value, "Test value")
self.assertEqual(item.comment, "Test comment")
class TestTranslationBatch(unittest.TestCase):
"""Test cases for TranslationBatch model"""
def setUp(self):
"""Set up test fixtures"""
self.items = [
TranslationItem(name="key1", value="Value 1"),
TranslationItem(name="key2", value="Value 2")
]
def test_translation_batch_creation(self):
"""Test creating a TranslationBatch"""
batch = TranslationBatch(
items=self.items,
target_language="values-de-rDE",
target_file="strings.xml"
)
self.assertEqual(len(batch.items), 2)
self.assertEqual(batch.target_language, "values-de-rDE")
self.assertEqual(batch.target_file, "strings.xml")
self.assertEqual(batch.items[0].name, "key1")
self.assertEqual(batch.items[1].value, "Value 2")
if __name__ == '__main__':
unittest.main()

188
tests/test_xml_processor.py Normal file
View File

@@ -0,0 +1,188 @@
"""
Tests for XML processor module
"""
import unittest
import tempfile
import os
from lxml import etree
from unittest.mock import patch, MagicMock
from config import Config
from models import TranslationItem
from xml_processor import XMLProcessor
class TestXMLProcessor(unittest.TestCase):
"""Test cases for XMLProcessor class"""
def setUp(self):
"""Set up test fixtures"""
# Create a mock config
self.mock_config = MagicMock(spec=Config)
self.mock_config.output_config = {
'create_backups': True,
'backup_suffix': '.backup'
}
self.processor = XMLProcessor(self.mock_config)
# Sample XML content (without XML declaration for testing)
self.sample_xml = '''<resources>
<string name="app_name">Test App</string>
<string name="welcome_message">Welcome to our app!</string>
<!-- This is a comment -->
<string name="button_ok">OK</string>
</resources>'''
def test_load_xml_file_success(self):
"""Test successful XML file loading"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
f.write(self.sample_xml)
temp_path = f.name
try:
root = self.processor.load_xml_file(temp_path)
self.assertIsNotNone(root)
self.assertEqual(root.tag, 'resources')
self.assertEqual(len(root.findall('string')), 3)
finally:
os.unlink(temp_path)
def test_load_xml_file_error(self):
"""Test error handling when loading invalid XML file"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
f.write('invalid xml content')
temp_path = f.name
try:
root = self.processor.load_xml_file(temp_path)
self.assertIsNone(root)
finally:
os.unlink(temp_path)
def test_extract_strings(self):
"""Test extracting strings from XML"""
root = etree.fromstring(self.sample_xml)
strings = self.processor.extract_strings(root)
self.assertEqual(len(strings), 3)
self.assertIn('app_name', strings)
self.assertIn('welcome_message', strings)
self.assertIn('button_ok', strings)
# Check string values
self.assertEqual(strings['app_name'].value, 'Test App')
self.assertEqual(strings['welcome_message'].value, 'Welcome to our app!')
self.assertEqual(strings['button_ok'].value, 'OK')
# Check that all items are TranslationItem instances
for item in strings.values():
self.assertIsInstance(item, TranslationItem)
def test_add_missing_strings(self):
"""Test adding missing strings to XML"""
# Create target XML with one existing string
target_xml = '''<resources>
<string name="existing_key">Existing value</string>
</resources>'''
target_root = etree.fromstring(target_xml)
missing_strings = [
('new_key1', 'New value 1', 'string', []),
('new_key2', 'New value 2', 'string', []),
('existing_key', 'Should not be added', 'string', []) # This should be ignored
]
self.processor.add_missing_strings(target_root, missing_strings)
# Check that new strings were added
strings = target_root.findall('string')
self.assertEqual(len(strings), 3) # 1 existing + 2 new
# Check specific strings
new_key1 = target_root.find(".//string[@name='new_key1']")
self.assertIsNotNone(new_key1)
self.assertEqual(new_key1.text, 'New value 1')
new_key2 = target_root.find(".//string[@name='new_key2']")
self.assertIsNotNone(new_key2)
self.assertEqual(new_key2.text, 'New value 2')
# Check existing string wasn't duplicated
existing_strings = target_root.findall(".//string[@name='existing_key']")
self.assertEqual(len(existing_strings), 1)
def test_extract_string_array(self):
"""Test extracting string arrays from XML"""
xml_with_array = '''<resources>
<string name="app_name">Test App</string>
<string-array name="vocabulary_hints">
<item>Basic greetings</item>
<item>Irregular verbs</item>
<item>Vocabulary at the airport</item>
</string-array>
</resources>'''
root = etree.fromstring(xml_with_array)
strings = self.processor.extract_strings(root)
self.assertEqual(len(strings), 2)
self.assertIn('app_name', strings)
self.assertIn('vocabulary_hints', strings)
# Check string array
array_item = strings['vocabulary_hints']
self.assertEqual(array_item.item_type, 'string-array')
self.assertEqual(len(array_item.items), 3)
self.assertEqual(array_item.items[0], 'Basic greetings')
self.assertEqual(array_item.items[1], 'Irregular verbs')
self.assertEqual(array_item.items[2], 'Vocabulary at the airport')
self.assertEqual(array_item.value, 'Basic greetings | Irregular verbs | Vocabulary at the airport')
def test_add_missing_string_array(self):
"""Test adding missing string arrays to XML"""
target_xml = '''<resources>
<string name="existing_key">Existing value</string>
</resources>'''
target_root = etree.fromstring(target_xml)
missing_strings = [
('vocabulary_hints', '', 'string-array', ['Basic greetings', 'Irregular verbs'])
]
self.processor.add_missing_strings(target_root, missing_strings)
# Check that string-array was added
string_array = target_root.find(".//string-array[@name='vocabulary_hints']")
self.assertIsNotNone(string_array)
# Check items
items = string_array.findall('item')
self.assertEqual(len(items), 2)
self.assertEqual(items[0].text, 'Basic greetings')
self.assertEqual(items[1].text, 'Irregular verbs')
def test_skip_non_translatable_string_array(self):
"""Test that non-translatable string arrays are skipped"""
xml_with_non_translatable = '''<resources>
<string-array name="translatable_array" translatable="true">
<item>Item 1</item>
</string-array>
<string-array name="non_translatable_array" translatable="false">
<item>Item 2</item>
</string-array>
</resources>'''
root = etree.fromstring(xml_with_non_translatable)
strings = self.processor.extract_strings(root)
self.assertEqual(len(strings), 1)
self.assertIn('translatable_array', strings)
self.assertNotIn('non_translatable_array', strings)
# Note: File saving tests are complex to mock properly due to lxml internals
# The core functionality is tested through integration tests
if __name__ == '__main__':
unittest.main()