292 lines
11 KiB
Python
292 lines
11 KiB
Python
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from doc_auto_translate import MrLabelGuard # noqa: E402
|
|
from doc_auto_translate import TranslationPlan # noqa: E402
|
|
from doc_auto_translate import _add_entire # noqa: E402
|
|
from doc_auto_translate import _parse_label_list # noqa: E402
|
|
from doc_auto_translate import _parse_trans_file # noqa: E402
|
|
from doc_auto_translate import build_plan # noqa: E402
|
|
from doc_auto_translate import is_include_only_zh_cn # noqa: E402
|
|
from doc_auto_translate import run_translation # noqa: E402
|
|
|
|
|
|
class TestIncludeOnlyZhCn(unittest.TestCase):
|
|
def test_single_en_include(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
p = Path(tmp) / 'docs/zh_CN/foo.rst'
|
|
p.parent.mkdir(parents=True)
|
|
p.write_text('.. include:: ../../../en/api-reference/foo.rst\n', encoding='utf-8')
|
|
self.assertTrue(is_include_only_zh_cn(str(p), Path(tmp)))
|
|
|
|
def test_with_link_to_translation(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
p = Path(tmp) / 'docs/zh_CN/security/vulnerabilities.rst'
|
|
p.parent.mkdir(parents=True)
|
|
p.write_text(
|
|
':link_to_translation:`en:[English]`\n\n.. include:: ../../en/security/vulnerabilities.rst\n',
|
|
encoding='utf-8',
|
|
)
|
|
self.assertTrue(is_include_only_zh_cn(str(p), Path(tmp)))
|
|
|
|
def test_chinese_body_is_not_include_only(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
p = Path(tmp) / 'docs/zh_CN/api-guides/external-ram.rst'
|
|
p.parent.mkdir(parents=True)
|
|
p.write_text('片外 RAM\n========\n\n简介\n', encoding='utf-8')
|
|
self.assertFalse(is_include_only_zh_cn(str(p), Path(tmp)))
|
|
|
|
|
|
class TestBuildPlanMergeBaseIncludeStub(unittest.TestCase):
|
|
@mock.patch(
|
|
'doc_auto_translate._load_tcf',
|
|
return_value={'specified_files': ['docs/en'], 'compare_branch': 'master'},
|
|
)
|
|
@mock.patch('doc_auto_translate._merge_base', return_value='base123')
|
|
@mock.patch(
|
|
'doc_auto_translate._changed_doc_paths',
|
|
return_value=['docs/en/security/tee/tee-ota.rst'],
|
|
)
|
|
@mock.patch('doc_auto_translate.is_include_only_zh_cn', return_value=False)
|
|
@mock.patch('doc_auto_translate.is_include_only_zh_cn_at_rev', return_value=True)
|
|
@mock.patch('doc_auto_translate._path_exists_at_rev', return_value=True)
|
|
def test_en_change_entire_when_merge_base_was_include_stub(
|
|
self,
|
|
_exists: mock.Mock,
|
|
_at_rev: mock.Mock,
|
|
_head: mock.Mock,
|
|
_changed: mock.Mock,
|
|
_mb: mock.Mock,
|
|
_tcf: mock.Mock,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
zh = root / 'docs/zh_CN/security/tee/tee-ota.rst'
|
|
zh.parent.mkdir(parents=True)
|
|
zh.write_text('TEE 空中更新\n========\n', encoding='utf-8')
|
|
plan, _ = build_plan(root)
|
|
self.assertEqual(plan.entire, ['docs/en/security/tee/tee-ota.rst'])
|
|
self.assertEqual(plan.incremental, [])
|
|
|
|
@mock.patch(
|
|
'doc_auto_translate._load_tcf',
|
|
return_value={'specified_files': ['docs/zh_CN'], 'compare_branch': 'master'},
|
|
)
|
|
@mock.patch('doc_auto_translate._merge_base', return_value='base123')
|
|
@mock.patch(
|
|
'doc_auto_translate._changed_doc_paths',
|
|
return_value=['docs/zh_CN/security/tee/tee-ota.rst'],
|
|
)
|
|
@mock.patch('doc_auto_translate.is_include_only_zh_cn', return_value=False)
|
|
@mock.patch('doc_auto_translate.is_include_only_zh_cn_at_rev', return_value=True)
|
|
def test_zh_change_entire_when_merge_base_was_include_stub(
|
|
self,
|
|
_at_rev: mock.Mock,
|
|
_head: mock.Mock,
|
|
_changed: mock.Mock,
|
|
_mb: mock.Mock,
|
|
_tcf: mock.Mock,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
plan, _ = build_plan(Path(tmp))
|
|
self.assertEqual(plan.entire, ['docs/en/security/tee/tee-ota.rst'])
|
|
self.assertEqual(plan.incremental, [])
|
|
|
|
@mock.patch(
|
|
'doc_auto_translate._load_tcf',
|
|
return_value={'specified_files': ['docs/en'], 'compare_branch': 'master'},
|
|
)
|
|
@mock.patch('doc_auto_translate._merge_base', return_value='base123')
|
|
@mock.patch(
|
|
'doc_auto_translate._changed_doc_paths',
|
|
return_value=[
|
|
'docs/en/security/tee/tee-ota.rst',
|
|
'docs/zh_CN/security/tee/tee-ota.rst',
|
|
],
|
|
)
|
|
@mock.patch('doc_auto_translate.is_include_only_zh_cn', return_value=True)
|
|
@mock.patch('doc_auto_translate.is_include_only_zh_cn_at_rev', return_value=False)
|
|
@mock.patch('doc_auto_translate._path_exists_at_rev', return_value=True)
|
|
def test_both_en_and_zh_change_entire_once(
|
|
self,
|
|
_exists: mock.Mock,
|
|
_at_rev: mock.Mock,
|
|
_head: mock.Mock,
|
|
_changed: mock.Mock,
|
|
_mb: mock.Mock,
|
|
_tcf: mock.Mock,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
zh = root / 'docs/zh_CN/security/tee/tee-ota.rst'
|
|
zh.parent.mkdir(parents=True)
|
|
zh.write_text('.. include:: ../../../en/security/tee/tee-ota.rst\n', encoding='utf-8')
|
|
plan, _ = build_plan(root)
|
|
self.assertEqual(plan.entire, ['docs/en/security/tee/tee-ota.rst'])
|
|
self.assertEqual(plan.incremental, [])
|
|
|
|
|
|
class TestAddEntire(unittest.TestCase):
|
|
def test_deduplicates_en_path(self) -> None:
|
|
plan = TranslationPlan()
|
|
_add_entire(plan, 'docs/en/foo.rst')
|
|
_add_entire(plan, 'docs/en/foo.rst')
|
|
self.assertEqual(plan.entire, ['docs/en/foo.rst'])
|
|
|
|
def test_zh_path_resolves_to_single_en_entry(self) -> None:
|
|
plan = TranslationPlan()
|
|
_add_entire(plan, 'docs/en/foo.rst')
|
|
_add_entire(plan, 'docs/zh_CN/foo.rst')
|
|
self.assertEqual(plan.entire, ['docs/en/foo.rst'])
|
|
|
|
|
|
class TestMrLabelGuard(unittest.TestCase):
|
|
def test_parse_label_list(self) -> None:
|
|
self.assertEqual(_parse_label_list('auto-translate::full'), ['auto-translate::full'])
|
|
self.assertEqual(
|
|
_parse_label_list('auto-translate::full, auto-translate::incremental'),
|
|
['auto-translate::full', 'auto-translate::incremental'],
|
|
)
|
|
|
|
@mock.patch.dict(os.environ, {'CI_MERGE_REQUEST_IID': '42'}, clear=False)
|
|
@mock.patch('doc_auto_translate._remove_mr_labels', return_value=['auto-translate::full'])
|
|
@mock.patch('doc_auto_translate._restore_mr_labels')
|
|
def test_restore_on_failure(self, restore: mock.Mock, remove: mock.Mock) -> None:
|
|
with self.assertRaises(RuntimeError):
|
|
with MrLabelGuard(['auto-translate::full']):
|
|
raise RuntimeError('translate failed')
|
|
remove.assert_called_once_with(['auto-translate::full'])
|
|
restore.assert_called_once_with(['auto-translate::full'])
|
|
|
|
@mock.patch.dict(os.environ, {'CI_MERGE_REQUEST_IID': '42'}, clear=False)
|
|
@mock.patch('doc_auto_translate._remove_mr_labels', return_value=['auto-translate::full'])
|
|
@mock.patch('doc_auto_translate._restore_mr_labels')
|
|
def test_no_restore_on_success(self, restore: mock.Mock, remove: mock.Mock) -> None:
|
|
with MrLabelGuard(['auto-translate::full']):
|
|
pass
|
|
remove.assert_called_once_with(['auto-translate::full'])
|
|
restore.assert_not_called()
|
|
|
|
@mock.patch.dict(os.environ, {}, clear=True)
|
|
@mock.patch('doc_auto_translate._remove_mr_labels')
|
|
def test_skip_without_mr(self, remove: mock.Mock) -> None:
|
|
with MrLabelGuard(['auto-translate::full']):
|
|
pass
|
|
remove.assert_not_called()
|
|
|
|
|
|
class TestTransFilePathNormalization(unittest.TestCase):
|
|
def test_normalize_zh_folder_aliases(self) -> None:
|
|
parsed = _parse_trans_file(
|
|
'["docs/zh_cn/security/tee/tee-ota.rst",'
|
|
' "docs/zh-cn/security/tee/tee-ota.rst",'
|
|
' "docs/zh-Hans/security/tee/tee-ota.rst"]'
|
|
)
|
|
self.assertEqual(
|
|
parsed,
|
|
[
|
|
'docs/zh_CN/security/tee/tee-ota.rst',
|
|
'docs/zh_CN/security/tee/tee-ota.rst',
|
|
'docs/zh_CN/security/tee/tee-ota.rst',
|
|
],
|
|
)
|
|
|
|
|
|
class TestRunTranslationBatchCommit(unittest.TestCase):
|
|
@mock.patch('doc_auto_translate._commit_translated_files')
|
|
@mock.patch('doc_auto_translate._translate_files_local')
|
|
def test_single_commit_for_mixed_buckets(
|
|
self,
|
|
translate_local: mock.Mock,
|
|
commit_files: mock.Mock,
|
|
) -> None:
|
|
plan = TranslationPlan(
|
|
incremental=['docs/en/a.rst'],
|
|
entire=['docs/en/b.rst'],
|
|
)
|
|
translate_local.side_effect = [
|
|
['docs/zh_CN/b.rst'],
|
|
['docs/zh_CN/a.rst'],
|
|
]
|
|
auto_dir = Path('/tmp/auto-translate')
|
|
|
|
run_translation(
|
|
plan,
|
|
auto_dir,
|
|
'key',
|
|
'https://gitlab.example.com',
|
|
'token',
|
|
'123',
|
|
'feature-branch',
|
|
'all',
|
|
diff_base_commit='abc123',
|
|
)
|
|
|
|
self.assertEqual(translate_local.call_count, 2)
|
|
translate_local.assert_any_call(
|
|
auto_dir,
|
|
['docs/en/b.rst'],
|
|
entire=True,
|
|
openai_key='key',
|
|
url='https://gitlab.example.com',
|
|
bot='token',
|
|
project_id='123',
|
|
branch='feature-branch',
|
|
)
|
|
translate_local.assert_any_call(
|
|
auto_dir,
|
|
['docs/en/a.rst'],
|
|
entire=False,
|
|
openai_key='key',
|
|
url='https://gitlab.example.com',
|
|
bot='token',
|
|
project_id='123',
|
|
branch='feature-branch',
|
|
diff_base_commit='abc123',
|
|
)
|
|
commit_files.assert_called_once_with(
|
|
auto_dir,
|
|
['docs/zh_CN/b.rst', 'docs/zh_CN/a.rst'],
|
|
'https://gitlab.example.com',
|
|
'token',
|
|
'123',
|
|
'feature-branch',
|
|
)
|
|
|
|
@mock.patch('doc_auto_translate._commit_translated_files')
|
|
@mock.patch('doc_auto_translate._translate_files_local')
|
|
def test_no_commit_when_nothing_translated(
|
|
self,
|
|
translate_local: mock.Mock,
|
|
commit_files: mock.Mock,
|
|
) -> None:
|
|
plan = TranslationPlan()
|
|
auto_dir = Path('/tmp/auto-translate')
|
|
|
|
run_translation(
|
|
plan,
|
|
auto_dir,
|
|
'key',
|
|
'https://gitlab.example.com',
|
|
'token',
|
|
'123',
|
|
'feature-branch',
|
|
'all',
|
|
)
|
|
|
|
translate_local.assert_not_called()
|
|
commit_files.assert_not_called()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|