Files
espressif--esp-idf/tools/ci/test/test_doc_auto_translate.py
2026-07-13 13:04:25 +08:00

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()