seitime-frappe/frappe/tests/test_translate.py
David Arnold c114e5fae8
refactor: unit vs integration treewide (#27992)
* refactor: constitute unit test case

* fix: docs and type hints

* refactor: mark presumed integration test cases explicitly

At time of writing, we now have at least two base test classes:

- frappe.tests.UnitTestCase
- frappe.tests.IntegrationTestCase

They load in their perspective priority queue during execution.

Probably more to come for more efficient queing and scheduling.

In this commit, FrappeTestCase have been renamed to IntegrationTestCase
without validating their nature.

* feat: Move test-related functions from test_runner.py to tests/utils.py

* refactor: add bare UnitTestCase to all doctype tests

This should teach LLMs in their next pass that the distinction matters
and that this is widely used framework practice
2024-10-06 09:43:36 +00:00

333 lines
10 KiB
Python

# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import textwrap
from random import choices
from unittest.mock import patch
import frappe
import frappe.translate
from frappe import _, _lt
from frappe.gettext.extractors.javascript import extract_javascript
from frappe.tests import IntegrationTestCase
from frappe.translate import (
MERGED_TRANSLATION_KEY,
USER_TRANSLATION_KEY,
clear_cache,
extract_messages_from_javascript_code,
extract_messages_from_python_code,
get_language,
get_messages_for_app,
get_parent_language,
get_translation_dict_from_file,
)
from frappe.utils import get_bench_path, set_request
dirname = os.path.dirname(__file__)
translation_string_file = os.path.abspath(os.path.join(dirname, "translation_test_file.txt"))
first_lang, second_lang, third_lang, fourth_lang, fifth_lang = choices(
# skip "en*" since it is a default language
frappe.get_all("Language", pluck="name", filters=[["name", "not like", "en%"], ["enabled", "=", 1]]),
k=5,
)
_lazy_translations = _lt("Communication")
class TestTranslate(IntegrationTestCase):
guest_sessions_required = (
"test_guest_request_language_resolution_with_cookie",
"test_guest_request_language_resolution_with_request_header",
)
def setUp(self):
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Guest")
def tearDown(self):
frappe.form_dict.pop("_lang", None)
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Administrator")
frappe.local.lang = "en"
def test_clear_cache(self):
_("Trigger caching")
self.assertIsNotNone(frappe.cache.hget(USER_TRANSLATION_KEY, frappe.local.lang))
self.assertIsNotNone(frappe.cache.hget(MERGED_TRANSLATION_KEY, frappe.local.lang))
clear_cache()
self.assertIsNone(frappe.cache.hget(USER_TRANSLATION_KEY, frappe.local.lang))
self.assertIsNone(frappe.cache.hget(MERGED_TRANSLATION_KEY, frappe.local.lang))
def test_extract_message_from_file(self):
data = frappe.translate.get_messages_from_file(translation_string_file)
bench_path = get_bench_path()
file_path = frappe.get_app_path("frappe", "tests", "translation_test_file.txt")
exp_filename = os.path.relpath(file_path, bench_path)
self.assertEqual(
len(data),
len(expected_output),
msg=f"Mismatched output:\nExpected: {expected_output}\nFound: {data}",
)
for extracted, expected in zip(data, expected_output, strict=False):
ext_filename, ext_message, ext_context, ext_line = extracted
exp_message, exp_context, exp_line = expected
self.assertEqual(ext_filename, exp_filename)
self.assertEqual(ext_message, exp_message)
self.assertEqual(ext_context, exp_context)
self.assertEqual(ext_line, exp_line)
def test_read_language_variant(self):
self.assertEqual(_("Mobile No"), "Mobile No")
try:
frappe.local.lang = "pt-BR"
self.assertEqual(_("Mobile No"), "Telefone Celular")
frappe.local.lang = "pt"
self.assertEqual(_("Mobile No"), "Nr. de Telemóvel")
finally:
frappe.local.lang = "en"
self.assertEqual(_("Mobile No"), "Mobile No")
def test_translation_with_context(self):
frappe.local.lang = "fr"
self.assertEqual(_("Change"), "Changement")
self.assertEqual(_("Change", context="Coins"), "la monnaie")
def test_lazy_translations(self):
frappe.local.lang = "de"
eager_translation = _("Communication")
self.assertEqual(str(_lazy_translations), eager_translation)
self.assertRaises(NotImplementedError, lambda: _lazy_translations == "blah")
# auto casts when added or radded
self.assertEqual(_lazy_translations + "A", eager_translation + "A")
x = _lazy_translations
x += "A"
self.assertEqual(x, eager_translation + "A")
# f string usually auto-casts
self.assertEqual(f"{_lazy_translations}", eager_translation)
def test_request_language_resolution_with_form_dict(self):
"""Test for frappe.translate.get_language
Case 1: frappe.form_dict._lang is set
"""
frappe.form_dict._lang = first_lang
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang):
return_val = get_language()
self.assertIn(return_val, [first_lang, get_parent_language(first_lang)])
def test_request_language_resolution_with_cookie(self):
"""Test for frappe.translate.get_language
Case 2: frappe.form_dict._lang is not set, but preferred_language cookie is
"""
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value="fr"):
set_request(method="POST", path="/", headers=[("Accept-Language", "hr")])
return_val = get_language()
# system default language
self.assertEqual(return_val, "en")
self.assertNotIn(return_val, [second_lang, get_parent_language(second_lang)])
def test_guest_request_language_resolution_with_cookie(self):
"""Test for frappe.translate.get_language
Case 3: frappe.form_dict._lang is not set, but preferred_language cookie is [Guest User]
"""
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang):
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertIn(return_val, [second_lang, get_parent_language(second_lang)])
def test_global_translations(self):
""" """
site = frappe.local.site
frappe.destroy()
_("this shouldn't break")
frappe.init(site)
frappe.connect()
def test_guest_request_language_resolution_with_request_header(self):
"""Test for frappe.translate.get_language
Case 4: frappe.form_dict._lang & preferred_language cookie is not set, but Accept-Language header is [Guest User]
"""
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertIn(return_val, [third_lang, get_parent_language(third_lang)])
def test_request_language_resolution_with_request_header(self):
"""Test for frappe.translate.get_language
Case 5: frappe.form_dict._lang & preferred_language cookie is not set, but Accept-Language header is
"""
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertNotIn(return_val, [third_lang, get_parent_language(third_lang)])
def test_load_all_translate_files(self):
"""Load all CSV files to ensure they have correct format"""
verify_translation_files("frappe")
def test_python_extractor(self):
code = textwrap.dedent(
"""
frappe._("attr")
_("name")
frappe._("attr with", context="attr context")
_("name with", context="name context")
_("broken on",
context="new line")
__("This wont be captured")
__init__("This shouldn't too")
_(
"broken on separate line",
)
_(not_a_string)
_(not_a_string, context="wat")
_lt("Communication")
"""
)
expected_output = [
(2, "attr", None),
(3, "name", None),
(4, "attr with", "attr context"),
(5, "name with", "name context"),
(6, "broken on", "new line"),
(10, "broken on separate line", None),
(15, "Communication", None),
]
output = extract_messages_from_python_code(code)
self.assertEqual(len(expected_output), len(output))
for expected, actual in zip(expected_output, output, strict=False):
with self.subTest():
self.assertEqual(expected, actual)
def test_js_extractor(self):
code = textwrap.dedent(
"""
__("attr")
__("attr with", null, "context")
__("attr with", ["format", "replacements"], "context")
__("attr with", ["format", "replacements"])
__(
"Long JS string with", [
"format", "replacements"
],
"JS context on newline"
)
__(
"Long JS string with formats only {0}", [
"format", "replacements"
],
)
_(`template strings not supported yet`)
"""
)
expected_output = [
(2, "attr", None),
(3, "attr with", "context"),
(4, "attr with", "context"),
(5, "attr with", None),
(6, "Long JS string with", "JS context on newline"),
(12, "Long JS string with formats only {0}", None),
]
output = extract_messages_from_javascript_code(code)
self.assertEqual(len(expected_output), len(output))
for expected, actual in zip(expected_output, output, strict=False):
with self.subTest():
self.assertEqual(expected, actual)
def test_js_parser_arg_capturing(self):
"""Get non-flattened args in correct order so 3rd arg if present is always context."""
def get_args(code):
*__, args = next(extract_javascript(code))
return args
args = get_args("""__("attr with", ["format", "replacements"], "context")""")
self.assertEqual(args, ("attr with", None, "context"))
args = get_args("""__("attr with", ["format", "replacements"])""")
self.assertEqual(args, "attr with")
args = get_args("""__("attr with", null, "context")""")
self.assertEqual(args, ("attr with", None, "context"))
args = get_args(
"""__(
"Multiline translation with format replacements and context {0} {1}",
[
"format",
call("replacements", {
"key": "value"
}),
],
"context"
)"""
)
self.assertEqual(
args, ("Multiline translation with format replacements and context {0} {1}", None, "context")
)
args = get_args(
"""__(
"Multiline translation with format replacements and no context {0} {1}",
[
"format",
call("replacements", {
"key": "value"
}),
],
)"""
)
self.assertEqual(
args, ("Multiline translation with format replacements and no context {0} {1}", None)
)
def verify_translation_files(app):
"""Function to verify translation file syntax in app."""
# Do not remove/rename this, other apps depend on it to test their translations
from pathlib import Path
translations_dir = Path(frappe.get_app_path(app)) / "translations"
for file in translations_dir.glob("*.csv"):
lang = file.stem # basename of file = lang
get_translation_dict_from_file(file, lang, app, throw=True)
get_messages_for_app(app)
expected_output = [
("Warning: Unable to find {0} in any table related to {1}", "This is some context", 2),
("Warning: Unable to find {0} in any table related to {1}", None, 4),
("You don't have any messages yet.", None, 6),
("Submit", "Some DocType", 8),
("Warning: Unable to find {0} in any table related to {1}", "This is some context", 15),
("Submit", "Some DocType", 17),
("You don't have any messages yet.", None, 19),
("You don't have any messages yet.", None, 21),
("Long string that needs its own line because of black formatting.", None, 24),
("Long string with", "context", 28),
("Long string with", "context on newline", 32),
]