Files
superset2/tests/unit_tests/scripts/translations/check_translation_regression_test.py
2026-06-03 14:47:31 -07:00

180 lines
7.2 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Tests for ``scripts/translations/check_translation_regression.py``.
The script is not installed as a package, so it is loaded via importlib from
its on-disk path.
"""
import importlib.util
import json # noqa: TID251 - testing a standalone script that uses stdlib json
from collections.abc import Mapping
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
_SCRIPT_PATH = (
Path(__file__).resolve().parents[4]
/ "scripts"
/ "translations"
/ "check_translation_regression.py"
)
_spec = importlib.util.spec_from_file_location(
"check_translation_regression", _SCRIPT_PATH
)
assert _spec is not None, f"Could not load {_SCRIPT_PATH}"
assert _spec.loader is not None, f"No loader on spec for {_SCRIPT_PATH}"
check_translation_regression = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(check_translation_regression)
def _compare(
tmp_path: Path,
before: Mapping[str, object],
after: Mapping[str, object],
failures: set[str] | None = None,
) -> None:
"""Run cmd_compare with a baseline file and a mocked 'after' state.
``failures`` simulates languages whose .po file was present but could not
be counted; cmd_compare passes a ``failures`` set into get_counts, so the
mock populates it (mirroring the real get_counts contract).
"""
def fake_get_counts(
_dir: Path, failures: set[str] | None = None
) -> Mapping[str, object]:
if failures is not None and _simulated_failures:
failures.update(_simulated_failures)
return after
_simulated_failures = failures or set()
before_file = tmp_path / "before.json"
before_file.write_text(json.dumps(before), encoding="utf-8")
with patch.object(
check_translation_regression, "get_counts", side_effect=fake_get_counts
):
check_translation_regression.cmd_compare(str(before_file), tmp_path)
def test_deleting_a_translated_string_is_not_a_regression(tmp_path: Path) -> None:
# Translated count drops by 1 (string removed from source) but no new
# fuzzy is introduced -> intentional deletion, must NOT be flagged.
before = {"fr": {"translated": 100, "fuzzy": 5}}
after = {"fr": {"translated": 99, "fuzzy": 5}}
_compare(tmp_path, before, after) # no SystemExit
def test_renaming_a_string_flags_a_regression(tmp_path: Path) -> None:
# A reworded source string strands its translation as fuzzy: translated
# drops by 1 AND fuzzy rises by 1 -> real regression.
before = {"fr": {"translated": 100, "fuzzy": 5}}
after = {"fr": {"translated": 99, "fuzzy": 6}}
with pytest.raises(SystemExit) as exc:
_compare(tmp_path, before, after)
assert exc.value.code == 1
def test_no_change_is_clean(tmp_path: Path) -> None:
stats = {"fr": {"translated": 100, "fuzzy": 5}}
_compare(tmp_path, stats, dict(stats)) # no SystemExit
def test_adding_strings_does_not_offset_a_regression(tmp_path: Path) -> None:
# Even when the PR adds new (untranslated) strings, a fresh fuzzy elsewhere
# is still a regression — additions must not mask it.
before = {"de": {"translated": 200, "fuzzy": 10}}
after = {"de": {"translated": 205, "fuzzy": 11}}
with pytest.raises(SystemExit) as exc:
_compare(tmp_path, before, after)
assert exc.value.code == 1
def test_legacy_integer_baseline_is_tolerated(tmp_path: Path) -> None:
# Older baselines stored a bare translated count with no fuzzy data.
before = {"fr": 100}
after = {"fr": {"translated": 90, "fuzzy": 0}}
_compare(tmp_path, before, after) # deletion-only, no SystemExit
def test_deleting_an_entire_catalog_is_not_a_regression(tmp_path: Path) -> None:
# The whole `fr` catalog was intentionally removed: it is absent from
# `after` and did NOT fail to count -> a legitimate deletion, not flagged.
before = {"fr": {"translated": 100, "fuzzy": 5}}
after: dict[str, object] = {}
_compare(tmp_path, before, after) # no SystemExit
def test_uncountable_baseline_catalog_is_a_hard_failure(tmp_path: Path) -> None:
# `fr` still exists on disk but msgfmt could not count it (malformed .po),
# so it is missing from `after` AND reported as a failure. This must NOT be
# mistaken for an intentional deletion — it is a hard error.
before = {"fr": {"translated": 100, "fuzzy": 5}}
after: dict[str, object] = {}
with pytest.raises(SystemExit) as exc:
_compare(tmp_path, before, after, failures={"fr"})
assert exc.value.code == 1
def test_uncountable_new_catalog_not_in_baseline_is_ignored(tmp_path: Path) -> None:
# A catalog that fails to count but was not in the baseline can't be a
# regression of a previously-good translation, so it does not fail the run.
before = {"fr": {"translated": 100, "fuzzy": 5}}
after = {"fr": {"translated": 100, "fuzzy": 5}}
_compare(tmp_path, before, after, failures={"zz"}) # no SystemExit
def test_writes_report_on_regression(tmp_path: Path) -> None:
before_file = tmp_path / "before.json"
before_file.write_text(json.dumps({"ja": {"translated": 50, "fuzzy": 2}}))
report = tmp_path / "report.md"
after = {"ja": {"translated": 49, "fuzzy": 3}}
with patch.object(check_translation_regression, "get_counts", return_value=after):
with pytest.raises(SystemExit):
check_translation_regression.cmd_compare(
str(before_file), tmp_path, str(report)
)
body = report.read_text(encoding="utf-8")
assert "Translation Regression Detected" in body
assert "`ja`" in body
# The report must make clear deletions are not flagged.
assert "deleting" in body.lower()
def test_count_stats_parses_translated_and_fuzzy() -> None:
stderr = (
"3731 translated messages, 1009 fuzzy translations, 175 untranslated messages."
)
mock_result = MagicMock(stderr=stderr)
with patch.object(
check_translation_regression.subprocess, "run", return_value=mock_result
):
stats = check_translation_regression.count_stats(Path("dummy.po"))
assert stats == {"translated": 3731, "fuzzy": 1009}
def test_count_stats_defaults_fuzzy_to_zero_when_absent() -> None:
# msgfmt omits the fuzzy clause entirely when there are none.
mock_result = MagicMock(stderr="42 translated messages.")
with patch.object(
check_translation_regression.subprocess, "run", return_value=mock_result
):
stats = check_translation_regression.count_stats(Path("dummy.po"))
assert stats == {"translated": 42, "fuzzy": 0}