chore: prune redundant tests and maintenance scripts

Remove low-value rendering, dependency-owned and script tests together with orphaned test scaffolding and obsolete wrappers. Keep state-driven visual coverage and release screenshot workflows.

Preserve incoming message origins on preparation failures, refresh tool presentation fixtures, and move shared SSH import coverage out of the iOS host test bundle to avoid duplicate static types.

Relocate the host icon input manifest, remove the heuristic AppStrings checker, and simplify localization CI.
This commit is contained in:
Kaede Akino committed 2026-10-19 10:45:11 +08:00
1 parent 5fcbba38aa
commit 11e6c7ca1c
135 files changed
+81 -13418

No files matched your search

-95
View File
@@ -1,95 +0,0 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import shutil
import sys
import tempfile
import unittest
SCRIPTS = Path(__file__).resolve().parents[1]
PROJECT = SCRIPTS.parent / "Termind.xcodeproj" / "project.pbxproj"
def load_script():
path = SCRIPTS / "bump-version.py"
spec = importlib.util.spec_from_file_location("bump_version", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
bump_version = load_script()
class VersionCalculationTests(unittest.TestCase):
def test_semantic_bumps(self) -> None:
self.assertEqual(bump_version.next_version("1.10.3", "patch"), "1.10.4")
self.assertEqual(bump_version.next_version("1.10.3", "minor"), "1.11.0")
self.assertEqual(bump_version.next_version("1.10.3", "major"), "2.0.0")
self.assertEqual(bump_version.next_version("1.10.3", "2.1.0"), "2.1.0")
def test_invalid_version_is_rejected(self) -> None:
for value in ("1.2", "v1.2.3", "1.02.3", "one.two.three"):
with self.subTest(value=value):
with self.assertRaises(ValueError):
bump_version.next_version("1.10.3", value)
class ProjectUpdateTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
self.project = Path(self.temporary_directory.name) / "project.pbxproj"
shutil.copy2(PROJECT, self.project)
def tearDown(self) -> None:
self.temporary_directory.cleanup()
def test_updates_all_scopes_and_aligns_widgets_with_tfi(self) -> None:
original = self.project.read_text(encoding="utf-8")
widgets = next(scope for scope in bump_version.SCOPES if scope.name == "Widgets")
drifted = bump_version._update_scope(
original, widgets, bump_version.VersionState("8.0.0", 99)
)
self.project.write_text(drifted, encoding="utf-8")
before, expected = bump_version.bump_project(self.project, "9.8.7")
updated = self.project.read_text(encoding="utf-8")
self.assertNotEqual(updated, drifted)
for scope in bump_version.SCOPES:
self.assertEqual(bump_version.read_state(updated, scope), expected[scope.name])
self.assertEqual(expected["Project"].build, before["Project"].build + 1)
self.assertEqual(expected["TFM"].build, before["TFM"].build + 1)
self.assertEqual(expected["TFI"].build, before["TFI"].build + 1)
self.assertEqual(expected["Widgets"], expected["TFI"])
self.assertTrue(all(state.marketing_version == "9.8.7" for state in expected.values()))
def test_dry_run_leaves_project_unchanged(self) -> None:
original = self.project.read_bytes()
_, expected = bump_version.bump_project(self.project, "patch", write=False)
self.assertEqual(self.project.read_bytes(), original)
self.assertEqual(
expected["Widgets"],
expected["TFI"],
)
def test_build_only_preserves_versions_and_aligns_widgets(self) -> None:
original = self.project.read_text(encoding="utf-8")
before, expected = bump_version.bump_project(self.project, "build")
updated = self.project.read_text(encoding="utf-8")
for name in ("Project", "TFM", "TFI"):
self.assertEqual(expected[name].marketing_version, before[name].marketing_version)
self.assertEqual(expected[name].build, before[name].build + 1)
self.assertEqual(expected["Widgets"], expected["TFI"])
for scope in bump_version.SCOPES:
self.assertEqual(bump_version.read_state(updated, scope), expected[scope.name])
self.assertNotEqual(updated, original)
if __name__ == "__main__":
unittest.main()
-246
View File
@@ -1,246 +0,0 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import unittest
SPEC = importlib.util.spec_from_file_location(
"check_localization", Path(__file__).resolve().parents[1] / "check-localization.py"
)
assert SPEC is not None and SPEC.loader is not None
CHECK = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(CHECK)
def catalog(key: str, value: str, state: str = "translated") -> dict:
return {
"sourceLanguage": "en",
"strings": {key: {"localizations": {"ja": {"stringUnit": {"state": state, "value": value}}}}},
}
def japanese_problems(data):
return CHECK.problems(data, languages=("ja",))
class JapaneseLocalizationTests(unittest.TestCase):
def test_rejects_spaces_in_both_directions_for_all_japanese_scripts(self):
for value in ["日本語 API", "API 日本語", "ひらがな SSH", "SSH ひらがな", "サーバ API", "API サーバ",
"サーバ\tSSH", "SSH\u00a0サーバ", "サーバ\u3000SSH", "3 台", "サーバ 3"]:
with self.subTest(value=value):
self.assertTrue(any("spacing" in p for p in japanese_problems(catalog("Label", value))))
def test_retains_english_protocol_spaces_and_japanese_label_spacing(self):
for key, value in [("Label", "ssh user@hostで接続"), ("Label", "種類 不明"), ("Label", "SSHサーバ"),
("Type %@", "種類 %@"), ("Type %@", "種類 %1$@")]:
with self.subTest(value=value):
self.assertEqual(japanese_problems(catalog(key, value)), [])
def test_type_exception_is_narrow(self):
self.assertTrue(japanese_problems(catalog("Label %@", "種類 %@")))
self.assertTrue(japanese_problems(catalog("Type %@", "種類 %@")))
self.assertTrue(japanese_problems(catalog("Type %@", "種類 %@の SSH鍵")))
def test_reordered_arguments_keep_their_original_types(self):
self.assertEqual(japanese_problems(catalog("%lld on %@", "%2$@で%1$lld件")), [])
self.assertTrue(japanese_problems(catalog("%lld on %@", "%1$@で%2$lld件")))
self.assertTrue(japanese_problems(catalog("%lld on %@", "%@で%lld件")))
def test_rejects_lost_arguments_and_literal_percent(self):
self.assertTrue(japanese_problems(catalog("%lld%% complete", "%lld完了")))
self.assertTrue(japanese_problems(catalog("Host %@", "ホスト")))
self.assertEqual(japanese_problems(catalog("%lld%% complete", "%lld%%完了")), [])
def test_uses_localized_english_value_for_metadata_keys(self):
data = catalog("NSCameraUsageDescription", "カメラを使用します。")
data["strings"]["NSCameraUsageDescription"]["localizations"]["en"] = {
"stringUnit": {"state": "translated", "value": "Use the camera."}
}
self.assertEqual(japanese_problems(data), [])
def test_rejects_missing_unfinished_and_empty_translations(self):
data = catalog("Settings", "設定")
data["strings"]["Settings"]["localizations"] = {}
self.assertTrue(japanese_problems(data))
self.assertTrue(japanese_problems(catalog("Settings", "設定", "needs_review")))
self.assertTrue(japanese_problems(catalog("Settings", "")))
self.assertEqual(japanese_problems(catalog("", "")), [])
def test_checks_plural_units_and_rejects_non_japanese_categories(self):
data = catalog("%lld files", "%lld個のファイル")
localizations = data["strings"]["%lld files"]["localizations"]
unit = localizations["ja"]
localizations["en"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "%lld file"}},
"other": {"stringUnit": {"state": "translated", "value": "%lld files"}},
}}}
self.assertTrue(japanese_problems(data))
localizations["ja"] = {"variations": {"plural": {"other": unit}}}
self.assertEqual(japanese_problems(data), [])
unit["stringUnit"]["value"] = "%lld ファイル"
self.assertTrue(japanese_problems(data))
unit["stringUnit"]["value"] = "%lld個のファイル"
localizations["ja"]["variations"]["plural"]["one"] = unit
self.assertTrue(japanese_problems(data))
def test_respects_nontranslatable_entries(self):
data = catalog("Identifier", "")
data["strings"]["Identifier"]["shouldTranslate"] = False
self.assertEqual(japanese_problems(data), [])
class ChineseLocalizationTests(unittest.TestCase):
def check(self, key, value, language):
data = catalog(key, value)
localizations = data["strings"][key]["localizations"]
localizations[language] = localizations.pop("ja")
return CHECK.problems(data, languages=(language,))
def test_preserves_chinese_spacing_and_punctuation_checks(self):
for language in ["zh-Hans", "zh-Hant"]:
for value in ["中文 SSH", "SSH 中文", "中文,", "中文: SSH", "SSH :中文"]:
with self.subTest(language=language, value=value):
self.assertTrue(self.check("Label", value, language))
self.assertEqual(self.check("Label", "中文SSH", language), [])
def test_preserves_type_subtitle_exception(self):
for language, label in [("zh-Hans", "类型"), ("zh-Hant", "類型")]:
self.assertEqual(self.check("Type %@", label + " %@", language), [])
self.assertTrue(self.check("Other %@", label + " %@", language))
def test_shared_validation_requires_every_supported_language(self):
data = catalog("Settings", "設定")
failures = CHECK.problems(data)
self.assertEqual(len(failures), len(CHECK.SUPPORTED_LANGUAGES) - 1)
self.assertTrue(any("zh-Hans" in failure for failure in failures))
self.assertTrue(any("zh-Hant" in failure for failure in failures))
class KoreanLocalizationTests(unittest.TestCase):
def make_catalog(self, key, value):
data = catalog(key, value)
localizations = data["strings"][key]["localizations"]
localizations["ko"] = localizations.pop("ja")
return data
def test_allows_korean_word_spacing_and_protocol_names(self):
for value in ["SSH 인증 실패", "파일 3개", "사용자 확인 요청 안 함", "암호 구문"]:
with self.subTest(value=value):
self.assertEqual(CHECK.problems(self.make_catalog("Label", value), languages=("ko",)), [])
def test_reordered_arguments_preserve_positions_and_types(self):
data = self.make_catalog("Command failed with exit code %lld after %@.",
"명령이 %2$@ 후 종료 코드 %1$lld로 실패했습니다.")
self.assertEqual(CHECK.problems(data, languages=("ko",)), [])
unit = data["strings"]["Command failed with exit code %lld after %@."]["localizations"]["ko"]["stringUnit"]
unit["value"] = "명령이 %@ 후 종료 코드 %lld로 실패했습니다."
self.assertTrue(any("format arguments" in p for p in CHECK.problems(data, languages=("ko",))))
def test_requires_only_other_for_counted_resources(self):
data = self.make_catalog("%lld files", "파일 %lld개")
localizations = data["strings"]["%lld files"]["localizations"]
unit = localizations["ko"]
localizations["en"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "%lld file"}},
"other": {"stringUnit": {"state": "translated", "value": "%lld files"}},
}}}
self.assertTrue(CHECK.problems(data, languages=("ko",)))
localizations["ko"] = {"variations": {"plural": {"other": unit}}}
self.assertEqual(CHECK.problems(data, languages=("ko",)), [])
localizations["ko"]["variations"]["plural"]["one"] = unit
self.assertTrue(CHECK.problems(data, languages=("ko",)))
def test_rejects_missing_and_unfinished_korean(self):
data = self.make_catalog("Settings", "설정")
unit = data["strings"]["Settings"]["localizations"]["ko"]["stringUnit"]
unit["state"] = "needs_review"
self.assertEqual(CHECK.problems(data, languages=("ko",)), ["'Settings': unfinished ko translation"])
del data["strings"]["Settings"]["localizations"]["ko"]
self.assertEqual(CHECK.problems(data, languages=("ko",)), ["'Settings': missing ko translation"])
class EuropeanLocalizationTests(unittest.TestCase):
def make_catalog(self, key, value, language):
return {
"sourceLanguage": "en",
"strings": {key: {"localizations": {
language: {"stringUnit": {"state": "translated", "value": value}},
}}},
}
def test_checks_coverage_state_and_positional_arguments(self):
for language in ("de", "fr"):
with self.subTest(language=language):
data = self.make_catalog("%lld on %@", "%2$@ : %1$lld", language)
self.assertEqual(CHECK.problems(data, languages=(language,)), [])
unit = data["strings"]["%lld on %@"]["localizations"][language]["stringUnit"]
unit["value"] = "%1$@ : %2$lld"
self.assertTrue(CHECK.problems(data, languages=(language,)))
unit["value"] = "%lld on %@"
unit["state"] = "needs_review"
self.assertTrue(CHECK.problems(data, languages=(language,)))
del data["strings"]["%lld on %@"]["localizations"][language]
self.assertTrue(CHECK.problems(data, languages=(language,)))
def test_requires_singular_and_plural_even_for_target_only_variations(self):
for language in ("de", "fr"):
data = self.make_catalog("%lld files", "%lld fichiers", language)
localizations = data["strings"]["%lld files"]["localizations"]
unit = localizations[language]
localizations[language] = {"variations": {"plural": {"other": unit}}}
self.assertTrue(CHECK.problems(data, languages=(language,)))
forms = localizations[language]["variations"]["plural"]
forms["one"] = unit
self.assertEqual(CHECK.problems(data, languages=(language,)), [])
forms["few"] = unit
self.assertTrue(CHECK.problems(data, languages=(language,)))
del forms["few"]
forms["many"] = unit
failures = CHECK.problems(data, languages=(language,))
self.assertEqual(bool(failures), language == "de")
def test_english_plural_requires_translated_plural_variations(self):
for language in ("de", "fr"):
data = self.make_catalog("%lld files", "%lld fichiers", language)
localizations = data["strings"]["%lld files"]["localizations"]
localizations["en"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "%lld file"}},
"other": {"stringUnit": {"state": "translated", "value": "%lld files"}},
}}}
self.assertTrue(CHECK.problems(data, languages=(language,)))
def test_each_plural_branch_preserves_format_arguments(self):
data = self.make_catalog("%lld files", "%lld fichiers", "fr")
localizations = data["strings"]["%lld files"]["localizations"]
localizations["fr"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "un fichier"}},
"other": localizations["fr"],
}}}
self.assertTrue(any("format arguments" in p for p in CHECK.problems(data, languages=("fr",))))
def test_preserves_european_typography_without_cjk_spacing_rules(self):
for language, value in [("fr", "Échec\u00a0: «\u00a0clé SSH\u00a0»\u202f?"), ("de", "Schlüssel „SSH“")]:
data = self.make_catalog("Label", value, language)
self.assertEqual(CHECK.problems(data, languages=(language,)), [])
def test_explicit_scope_can_limit_checks_to_german_and_french(self):
data = self.make_catalog("Settings", "Einstellungen", "de")
self.assertEqual(CHECK.problems(data, languages=("de", "fr")), ["'Settings': missing fr translation"])
data["strings"]["Settings"]["localizations"]["fr"] = {
"stringUnit": {"state": "translated", "value": "Réglages"}
}
self.assertEqual(CHECK.problems(data, languages=("de", "fr")), [])
def test_uses_english_metadata_value_and_skips_nontranslatable_entries(self):
data = self.make_catalog("BundleKey", "Datei %@", "de")
data["strings"]["BundleKey"]["localizations"]["en"] = {
"stringUnit": {"state": "translated", "value": "File %@"}
}
self.assertEqual(CHECK.problems(data, languages=("de",)), [])
data["strings"]["Identifier"] = {"shouldTranslate": False}
self.assertEqual(CHECK.problems(data, languages=("de",)), [])
if __name__ == "__main__":
unittest.main()
@@ -1,383 +0,0 @@
from __future__ import annotations
import copy
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
SCRIPTS = Path(__file__).resolve().parents[1]
NORMALIZER = SCRIPTS / "normalize-string-catalogs.py"
class StringCatalogNormalizationTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
self.directory = Path(self.temporary_directory.name)
def tearDown(self) -> None:
self.temporary_directory.cleanup()
def run_normalizer(
self, mode: str, *paths: Path, script: Path = NORMALIZER
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(script), mode, *(str(path) for path in paths)],
cwd=self.directory,
capture_output=True,
text=True,
check=False,
)
def assert_success(self, result: subprocess.CompletedProcess[str]) -> None:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def write_catalog(self, name: str, content: object) -> Path:
path = self.directory / name
path.write_text(json.dumps(content, ensure_ascii=False), encoding="utf-8")
return path
def test_check_reports_drift_without_writing(self) -> None:
path = self.write_catalog(
"Localizable.xcstrings",
{
"version": "1.0",
"strings": {"Hello": {"extractionState": "stale"}},
"sourceLanguage": "en",
},
)
original = path.read_bytes()
original_mtime = path.stat().st_mtime_ns
result = self.run_normalizer("--check", path)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertEqual(path.read_bytes(), original)
self.assertEqual(path.stat().st_mtime_ns, original_mtime)
def test_check_detects_formatting_drift_when_entries_are_already_manual(self) -> None:
path = self.write_catalog(
"Formatting.xcstrings",
{
"sourceLanguage": "en",
"strings": {"Hello": {"extractionState": "manual"}},
"version": "1.0",
},
)
original = path.read_bytes()
result = self.run_normalizer("--check", path)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertEqual(path.read_bytes(), original)
def test_write_uses_xcode_spacing_unicode_and_no_final_newline(self) -> None:
path = self.write_catalog(
"Localizable.xcstrings",
{
"version": "1.0",
"strings": {
"你好 / Hello": {
"extractionState": "stale",
"comment": "中文 / slash, \"quote\", newline\nnext",
}
},
"sourceLanguage": "en",
},
)
self.assert_success(self.run_normalizer("--write", path))
self.assertEqual(
path.read_text(encoding="utf-8"),
'{\n'
' "sourceLanguage" : "en",\n'
' "strings" : {\n'
' "你好 / Hello" : {\n'
' "comment" : "中文 / slash, \\"quote\\", newline\\nnext",\n'
' "extractionState" : "manual"\n'
' }\n'
' },\n'
' "version" : "1.0"\n'
'}',
)
self.assert_success(self.run_normalizer("--check", path))
def test_empty_objects_use_xcode_blank_line_format(self) -> None:
path = self.write_catalog(
"Empty.xcstrings",
{"sourceLanguage": "en", "strings": {}, "version": "1.0"},
)
self.assert_success(self.run_normalizer("--write", path))
self.assertEqual(
path.read_text(encoding="utf-8"),
'{\n'
' "sourceLanguage" : "en",\n'
' "strings" : {\n'
'\n'
' },\n'
' "version" : "1.0"\n'
'}',
)
def test_nested_empty_arrays_and_objects_keep_xcode_indentation(self) -> None:
path = self.write_catalog(
"EmptyMetadata.xcstrings",
{
"sourceLanguage": "en",
"strings": {"Hello": {"metadata": {"emptyArray": [], "emptyObject": {}}}},
"version": "1.0",
},
)
self.assert_success(self.run_normalizer("--write", path))
self.assertIn(
' "metadata" : {\n'
' "emptyArray" : [\n'
'\n'
' ],\n'
' "emptyObject" : {\n'
'\n'
' }\n'
' }',
path.read_text(encoding="utf-8"),
)
def test_normalization_preserves_translations_variations_and_unknown_metadata(self) -> None:
catalog = {
"version": "1.2",
"sourceLanguage": "en",
"strings": {
"Deleted key": {"extractionState": "stale", "comment": "Keep this entry."},
"Items: %#@items@": {
"extractionState": "extracted_with_value",
"comment": "A count with plural forms.",
"shouldTranslate": True,
"localizations": {
"zh-Hant": {
"substitutions": {
"items": {
"argNum": 1,
"formatSpecifier": "lld",
"variations": {
"plural": {
"other": {
"stringUnit": {
"state": "translated",
"value": "%lld 個項目",
}
}
}
},
}
},
"stringUnit": {"state": "needs_review", "value": "項目:%#@items@"},
},
"en": {
"variations": {
"plural": {
"other": {"stringUnit": {"state": "new", "value": "%lld items"}},
"one": {"stringUnit": {"state": "translated", "value": "%lld item"}},
}
}
},
},
"metadata": {
"extractionState": "stale",
"ordered": [3, "三", {"z": False, "a": None}, ["b", "a"]],
"fraction": 1.25,
"empty": {},
},
},
"Implicit": {},
"Already manual": {"extractionState": "manual", "shouldTranslate": False},
},
"metadata": {"extractionState": "root_metadata", "futureField": ["z", "a"]},
}
expected = copy.deepcopy(catalog)
for entry in expected["strings"].values():
entry["extractionState"] = "manual"
path = self.write_catalog("Complex.xcstrings", catalog)
self.assert_success(self.run_normalizer("--write", path))
self.assertEqual(json.loads(path.read_text(encoding="utf-8")), expected)
def test_every_object_uses_unicode_code_point_order(self) -> None:
keys = ["😀", "\ue000", "中", "é", "a", "Z", "A", "", "a/b"]
path = self.write_catalog(
"Ordering.xcstrings",
{
"version": "1.0",
"strings": {key: {"z": 1, "a": 2} for key in keys},
"metadata": [{key: index for index, key in enumerate(keys)}],
"sourceLanguage": "en",
},
)
self.assert_success(self.run_normalizer("--write", path))
def check_order(value: object) -> None:
if isinstance(value, dict):
self.assertEqual(list(value), sorted(value))
for child in value.values():
check_order(child)
elif isinstance(value, list):
for child in value:
check_order(child)
check_order(json.loads(path.read_text(encoding="utf-8")))
def test_second_write_does_not_touch_content_or_mtime(self) -> None:
path = self.write_catalog(
"Stable.xcstrings",
{"sourceLanguage": "en", "strings": {"Hello": {}}, "version": "1.0"},
)
self.assert_success(self.run_normalizer("--write", path))
original = path.read_bytes()
sentinel_mtime = 1_600_000_000_123_456_789
os.utime(path, ns=(sentinel_mtime, sentinel_mtime))
original_mtime = path.stat().st_mtime_ns
self.assert_success(self.run_normalizer("--write", path))
self.assertEqual(path.read_bytes(), original)
self.assertEqual(path.stat().st_mtime_ns, original_mtime)
def test_batch_validation_finishes_before_any_file_is_written(self) -> None:
valid_path = self.write_catalog(
"First.xcstrings",
{"sourceLanguage": "en", "strings": {"Hello": {}}, "version": "1.0"},
)
original = valid_path.read_bytes()
original_mtime = valid_path.stat().st_mtime_ns
invalid_path = self.directory / "Last.xcstrings"
invalid_path.write_text('{"sourceLanguage": "en",', encoding="utf-8")
invalid_original = invalid_path.read_bytes()
result = self.run_normalizer("--write", valid_path, invalid_path)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(valid_path.read_bytes(), original)
self.assertEqual(valid_path.stat().st_mtime_ns, original_mtime)
self.assertEqual(invalid_path.read_bytes(), invalid_original)
def test_missing_batch_file_does_not_partially_write_or_create_files(self) -> None:
path = self.write_catalog(
"First.xcstrings",
{"sourceLanguage": "en", "strings": {"Hello": {}}, "version": "1.0"},
)
original = path.read_bytes()
missing = self.directory / "Missing.xcstrings"
result = self.run_normalizer("--write", path, missing)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(path.read_bytes(), original)
self.assertFalse(missing.exists())
def test_duplicate_keys_are_rejected_at_every_depth(self) -> None:
fixtures = [
'{"sourceLanguage":"en","sourceLanguage":"zh-Hans","strings":{},"version":"1.0"}',
'{"sourceLanguage":"en","strings":{"Hello":{},"Hello":{}},"version":"1.0"}',
'{"sourceLanguage":"en","strings":{"Hello":{"comment":"a","comment":"b"}},"version":"1.0"}',
'{"sourceLanguage":"en","strings":{},"version":"1.0","metadata":[{"a":1,"a":2}]}',
'{"sourceLanguage":"en","strings":{},"version":"1.0","metadata":{"a":1,"\\u0061":2}}',
]
for index, fixture in enumerate(fixtures):
with self.subTest(fixture=fixture):
path = self.directory / f"Duplicate{index}.xcstrings"
path.write_text(fixture, encoding="utf-8")
original = path.read_bytes()
result = self.run_normalizer("--write", path)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(path.read_bytes(), original)
def test_nonstandard_numeric_constants_are_rejected(self) -> None:
for value in ("NaN", "Infinity", "-Infinity"):
with self.subTest(value=value):
path = self.directory / "Nonstandard.xcstrings"
path.write_text(
'{"sourceLanguage":"en","strings":{},"version":"1.0","metadata":'
+ value
+ '}',
encoding="utf-8",
)
original = path.read_bytes()
result = self.run_normalizer("--write", path)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(path.read_bytes(), original)
def test_invalid_catalog_structure_is_rejected_without_writing(self) -> None:
valid = {"sourceLanguage": "en", "strings": {}, "version": "1.0"}
fixtures: list[object] = [[], None, "catalog", 1]
for field, values in {
"sourceLanguage": [None, "", 1, []],
"strings": [None, "", 1, []],
"version": [None, "", 1, []],
}.items():
missing = valid.copy()
del missing[field]
fixtures.append(missing)
for value in values:
fixtures.append({**valid, field: value})
for entry in (None, "translation", 1, []):
fixtures.append({**valid, "strings": {"Hello": entry}})
for fixture in fixtures:
with self.subTest(fixture=fixture):
path = self.write_catalog("Invalid.xcstrings", fixture)
original = path.read_bytes()
result = self.run_normalizer("--write", path)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(path.read_bytes(), original)
def test_default_paths_are_anchored_to_script_repository_and_limited_to_owned_catalogs(self) -> None:
repository = self.directory / "Repository"
scripts = repository / "Scripts"
scripts.mkdir(parents=True)
script = scripts / NORMALIZER.name
shutil.copy2(NORMALIZER, script)
resources = repository / "Frameworks" / "Resources"
resources.mkdir(parents=True)
fixture = '{"version":"1.0","strings":{"Hello":{}},"sourceLanguage":"en"}'
default_paths = [resources / "Localizable.xcstrings", resources / "InfoPlist.xcstrings"]
untouched_paths = [
resources / "Other.xcstrings",
self.directory / "AgentKit" / "Localizations" / "Localizable.xcstrings",
self.directory / "Localizable.xcstrings",
]
for path in default_paths + untouched_paths:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(fixture, encoding="utf-8")
self.assert_success(self.run_normalizer("--write", script=script))
for path in default_paths:
with self.subTest(path=path):
self.assertEqual(
json.loads(path.read_text(encoding="utf-8"))["strings"]["Hello"]["extractionState"],
"manual",
)
for path in untouched_paths:
with self.subTest(path=path):
self.assertEqual(path.read_text(encoding="utf-8"), fixture)
self.assert_success(self.run_normalizer("--check", script=script))
if __name__ == "__main__":
unittest.main()
@@ -1,119 +0,0 @@
from __future__ import annotations
import argparse
import importlib.util
from pathlib import Path
import socket
import sys
import threading
import unittest
SCRIPTS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS))
from port_forwarding_benchmark_common import endpoint_measurement, summarize
def load_script(name: str):
path = SCRIPTS / name
spec = importlib.util.spec_from_file_location(path.stem, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
termind_benchmark = load_script("benchmark-port-forwarding.py")
external_benchmark = load_script("benchmark-external-port-forwarding.py")
class TermindArgumentsTests(unittest.TestCase):
def test_default_and_unique_connection_counts(self) -> None:
arguments = termind_benchmark.make_parser().parse_args(
["--metadata", "fixture.json", "--output", "result.json"]
)
self.assertEqual(arguments.termind_connections, [1])
self.assertEqual(termind_benchmark.unique_counts([1, 64, 1, 4]), [1, 64, 4])
def test_connection_count_accepts_boundaries(self) -> None:
self.assertEqual(termind_benchmark.connection_count("1"), 1)
self.assertEqual(termind_benchmark.connection_count("64"), 64)
def test_connection_count_rejects_invalid_values(self) -> None:
for value in ("0", "65", "not-a-number"):
with self.subTest(value=value):
with self.assertRaises(argparse.ArgumentTypeError):
termind_benchmark.connection_count(value)
class ExternalArgumentsTests(unittest.TestCase):
def test_system_and_manual_modes_are_distinct(self) -> None:
parser = external_benchmark.make_parser()
system = parser.parse_args(
["--metadata", "fixture.json", "--output", "result.json", "system-ssh"]
)
manual = parser.parse_args(
[
"--metadata",
"fixture.json",
"--output",
"result.json",
"external",
"--name",
"Example",
"--kind",
"dynamic",
"--port",
"1080",
]
)
self.assertEqual(system.mode, "system-ssh")
self.assertEqual((manual.mode, manual.kind, manual.port), ("external", "dynamic", 1080))
class WorkloadTests(unittest.TestCase):
def test_manual_endpoint_reports_network_metrics_only(self) -> None:
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen()
stopped = threading.Event()
def serve() -> None:
while not stopped.is_set():
try:
connection, _ = listener.accept()
except OSError:
return
threading.Thread(target=echo, args=(connection,), daemon=True).start()
def echo(connection: socket.socket) -> None:
with connection:
while data := connection.recv(65_536):
connection.sendall(data)
thread = threading.Thread(target=serve, daemon=True)
thread.start()
try:
record = endpoint_measurement(
"external:Example",
listener.getsockname()[1],
"local",
streams=2,
target_port=0,
rounds=1,
)
finally:
stopped.set()
listener.close()
self.assertEqual(record["ssh_connections"], 1)
self.assertEqual(record["jumps"], 0)
self.assertNotIn("cpu_s", record)
self.assertNotIn("forwarder_peak_rss_mib", record)
summary = summarize([record])
self.assertEqual(summary[0]["samples"], 1)
self.assertNotIn("median_cpu_s", summary[0])
if __name__ == "__main__":
unittest.main()
-546
View File
@@ -1,546 +0,0 @@
require "minitest/autorun"
require "tmpdir"
require_relative "../../fastlane/lib/screenshots"
class ScreenshotsTests < Minitest::Test
def setup
@root = Dir.mktmpdir("termind-screenshots-")
FileUtils.mkdir_p(File.join(@root, "com.akinokaede.termind/assets"))
File.write(File.join(@root, "com.akinokaede.termind/.git"), "gitdir: fixture")
end
def teardown
FileUtils.remove_entry(@root)
end
def runner(platform = :ios, options = {}, environment = {})
Screenshots.new(platform, options, environment: environment, root: @root)
end
def test_default_capture_fails_before_build_when_submodule_is_missing
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
error = assert_raises(RuntimeError) { runner.run { flunk "scan must not start" } }
assert_includes error.message, "git submodule update --init com.akinokaede.termind"
refute Dir.exist?(File.join(@root, ".build"))
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_explicit_capture_output_can_publish_without_submodule
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
source = File.join(@root, "capture.png")
File.binwrite(source, "review capture")
capture = runner(:ios, output: "review")
capture.promote([[source, "en/ipad-15/vault.png"]])
assert_equal "review capture", File.binread(File.join(@root, "review/en/ipad-15/vault.png"))
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_lane_options_override_legacy_selectors_and_ipad_only_needs_no_iphone
capture = runner(:ios, { devices: "ipad", locales: "zh-Hant", scenes: "ssh,sftp" },
{ "TERMIND_SCREENSHOT_DEVICES" => "iphone", "TERMIND_SCREENSHOT_LOCALES" => "en" })
capture.define_singleton_method(:command) do |*args|
JSON.generate("devices" => { "com.apple.CoreSimulator.SimRuntime.iOS-27-0" => [
{ "name" => "iPad Pro 13-inch (M5)", "udid" => "ipad-id",
"deviceTypeIdentifier" => "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-12GB" }
] })
end
assert_equal ["ipad"], capture.find_devices.keys
assert_equal 2, capture.plan[:captures]
assert_equal ["zh-Hant"], capture.plan[:locales]
end
def test_runtime_defaults_follow_device_compatibility_and_allow_explicit_overrides
assert_equal "com.apple.CoreSimulator.SimRuntime.iOS-27-0", runner.plan[:runtime]
assert_equal "com.apple.CoreSimulator.SimRuntime.iOS-27-1", runner(:ios, devices: "iphone-duo-inner").plan[:runtime]
assert_equal "com.apple.CoreSimulator.SimRuntime.iOS-27-0",
runner(:ios, { devices: "iphone-duo-inner", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-27-0" }).plan[:runtime]
end
def test_a_renamed_pro_max_cannot_be_used_as_the_pro_capture_device
capture = runner(:ios, devices: "iphone")
capture.define_singleton_method(:command) do |*args|
JSON.generate("devices" => { "com.apple.CoreSimulator.SimRuntime.iOS-27-0" => [
{ "name" => "iPhone 18 Pro", "udid" => "wrong-model",
"deviceTypeIdentifier" => "com.apple.CoreSimulator.SimDeviceType.iPhone-18-Pro-Max" }
] })
end
assert_raises(RuntimeError) { capture.find_devices }
capture.define_singleton_method(:dimensions) { |_| [1320, 2868] }
assert_raises(RuntimeError) do
capture.validate_dimensions("pro-max.png", Screenshots::DEVICES.fetch("iphone").fetch(:dimensions))
end
end
def test_rejects_invalid_duplicate_and_empty_members_before_build
["ssh,ssh", "ssh,", "bogus"].each do |scenes|
assert_raises(ArgumentError) { runner(:ios, scenes: scenes) }
end
assert_raises(ArgumentError) { runner(:mac, scenes: "metrics-detail") }
assert_raises(ArgumentError) { runner(:ios, locales: "zh") }
assert_raises(ArgumentError) { runner(:ios, devices: "watch") }
assert_raises(ArgumentError) { runner(:ios, runtime: "com.apple.CoreSimulator.SimRuntime.iOS-26-0") }
end
def test_generated_xctestrun_preserves_host_settings_and_other_targets
capture = runner
fixture = { "EnvironmentVariables" => { "EXISTING" => "preserved" }, "TestHostPath" => "__TESTROOT__/App.app" }
document = { "TFITests" => fixture, "FrameworkTests" => { "CommandLineArguments" => ["unchanged"] } }
capture.configure_xctestrun(document, "zh-Hans", { "TERMIND_SCREENSHOTS" => "1" }, [])
assert_equal "preserved", fixture["EnvironmentVariables"]["EXISTING"]
assert_equal "__TESTROOT__/App.app", fixture["TestHostPath"]
assert_equal "CN", fixture["TestRegion"]
assert_equal ["-AppleLanguages", "(zh-Hans)", "-AppleLocale", "zh_CN"], fixture["CommandLineArguments"]
assert_equal ["unchanged"], document["FrameworkTests"]["CommandLineArguments"]
end
def test_japanese_capture_configures_japan_region_and_locale
capture = runner(:ios, locales: "ja")
target = {}
capture.configure_xctestrun({ "TFITests" => target }, "ja", {}, [])
assert_equal "ja", target["TestLanguage"]
assert_equal "JP", target["TestRegion"]
assert_equal ["-AppleLanguages", "(ja)", "-AppleLocale", "ja_JP"], target["CommandLineArguments"]
assert ScreenshotFrames.new({ locales: "ja" }, root: @root).plan.all? { |job| job[:output].include?("/ja/") }
end
def test_additional_marketing_locales_configure_native_capture_and_frame_paths
{ "ko" => ["KR", "ko_KR"], "de" => ["DE", "de_DE"], "fr" => ["FR", "fr_FR"] }.each do |language, (region, locale)|
%i[mac ios].each do |platform|
capture = runner(platform, locales: language)
target = {}
key = platform == :mac ? "TFMTests" : "TFITests"
capture.configure_xctestrun({ key => target }, language, {}, [])
assert_equal language, target["TestLanguage"]
assert_equal region, target["TestRegion"]
assert_equal "C", target["EnvironmentVariables"]["LC_NUMERIC"]
assert_equal ["-AppleLanguages", "(#{language})", "-AppleLocale", locale], target["CommandLineArguments"]
end
jobs = ScreenshotFrames.new({ locales: language }, root: @root).plan
assert jobs.all? { |job| job[:input].include?("/#{language}/") && job[:output].include?("/#{language}/") }
end
end
def test_test_plan_format_updates_only_the_capture_target
capture = runner(:mac)
target = { "BlueprintName" => "TFMTests" }
other = { "BlueprintName" => "FrameworkTests" }
document = { "TestConfigurations" => [{ "TestTargets" => [target, other] }] }
capture.configure_xctestrun(document, "zh-Hant", {}, ["--termind-demo=hosts:zh-Hant"])
assert_equal "TW", target["TestRegion"]
assert_equal "--termind-demo=hosts:zh-Hant", target["CommandLineArguments"].first
refute other.key?("TestLanguage")
end
def test_scan_configuration_pins_dependencies_and_serializes_hosted_tests
options = runner.scan_options("id=ipad", "vault-en")
assert_includes options[:xcargs], "-onlyUsePackageVersionsFromResolvedFile"
assert options[:skip_package_dependencies_resolution]
assert options[:disable_package_automatic_updates]
refute options[:parallel_testing]
assert options[:disable_concurrent_testing]
assert_equal 0, options[:number_of_retries]
end
def test_successful_capture_backs_up_original_bytes_and_manifest
capture = runner
original = File.join(@root, "com.akinokaede.termind/assets/captures/en/ipad-15/vault.png")
FileUtils.mkdir_p(File.dirname(original))
File.binwrite(original, "old image bytes")
source = File.join(@root, "new.png")
File.binwrite(source, "new image bytes")
capture.promote([[source, "en/ipad-15/vault.png"]])
assert_equal "new image bytes", File.binread(original)
backup = Dir.glob(File.join(@root, ".build/screenshot-backups/*/en/ipad-15/vault.png")).fetch(0)
assert_equal "old image bytes", File.binread(backup)
manifest = File.read(File.join(File.dirname(File.dirname(File.dirname(backup))), "SHA256SUMS"))
assert_equal "#{Digest::SHA256.hexdigest('old image bytes')} en/ipad-15/vault.png\n", manifest
end
def test_mac_captures_publish_localized_underscore_filenames
{ "command" => "command_generator", "metrics" => "metrics_overview" }.each do |scene, filename|
capture = runner(:mac, scenes: scene, locales: "zh-Hant")
capture.define_singleton_method(:test_configuration) { |*| File.join(@root, "test.xctestrun") }
capture.define_singleton_method(:capture_during_test) do |options, &block|
log = File.join(options.fetch(:buildlog_path), "Termind-TFM.log")
FileUtils.mkdir_p(File.dirname(log))
File.write(log, "TERMIND_MAC_SCREENSHOT_READY #{scene}\tzh-Hant\t123\ttoken\tactive\tkey\t2360\t1480\n")
block.call
end
capture.define_singleton_method(:command) do |*args|
raise "Unexpected command" unless args.first == "screencapture"
FileUtils.mkdir_p(File.dirname(args.last))
File.binwrite(args.last, "captured #{scene}")
end
capture.define_singleton_method(:dimensions) { |_| [2360, 1480] }
capture.capture_mac(scene, "zh-Hant", "platform=macOS")
destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hant/mac-14/#{filename}.png")
assert_equal "captured #{scene}", File.binread(destination)
end
end
def test_mac_camera_strip_is_padded_without_stretching_full_screen_capture
capture = runner(:mac, scenes: "workspace", locales: "en")
capture.define_singleton_method(:test_configuration) { |*| File.join(@root, "test.xctestrun") }
capture.define_singleton_method(:capture_during_test) do |options, &block|
log = File.join(options.fetch(:buildlog_path), "Termind-TFM.log")
FileUtils.mkdir_p(File.dirname(log))
File.write(log, "TERMIND_MAC_SCREENSHOT_READY workspace\ten\t123\ttoken\tactive\tkey\t3024\t1964\n")
block.call
end
calls = []
capture.define_singleton_method(:command) do |*args|
calls << args
if args.first == "screencapture"
FileUtils.mkdir_p(File.dirname(args.last))
File.binwrite(args.last, "full-screen window")
end
end
sizes = [[3024, 1890], [3024, 1964]]
capture.define_singleton_method(:dimensions) { |_| sizes.shift }
capture.capture_mac("workspace", "en", "platform=macOS")
padding = calls.find { |args| args.first == "magick" }
assert_includes padding, "3024x1964"
assert_includes padding, "south"
refute_includes padding, "-resize"
assert_equal ["screencapture", "-l", "123", "-o"], calls.first.first(4)
end
def test_mobile_capture_rediscovers_container_after_host_reinstallation
%w[iphone ipad iphone-duo-outer iphone-duo-inner].each do |device|
assert_mobile_capture_rediscovers_container(device)
end
end
def assert_mobile_capture_rediscovers_container(device)
capture = runner(:ios, devices: device, scenes: "vault", locales: "zh-Hans")
old_container = File.join(@root, "old-container")
new_container = File.join(@root, "new-container")
control = File.join(new_container, "Documents", "screenshot-capture-control")
token = "11111111-1111-1111-1111-111111111111"
capture.define_singleton_method(:test_configuration) do |_, _, environment|
FileUtils.mkdir_p(control)
File.write(File.join(control, "ready"), ["vault", environment.fetch("TERMIND_SCREENSHOT_DEVICE"), "zh-hans", token,
environment.fetch("TERMIND_SCREENSHOT_CAPTURE_ID")].join("\t"))
File.join(@root, "fixture.xctestrun")
end
capture.define_singleton_method(:capture_during_test) { |_, &block| 2.times { block.call } }
commands = []
capture.define_singleton_method(:sleep) { |_| }
capture.define_singleton_method(:command) do |*args|
commands << args
if args.include?("displays")
next JSON.generate("result" => { "displays" => [
{ "active" => true, "nativeSize" => Screenshots::DEVICES.fetch(device).fetch(:dimensions).sort,
"uniqueId" => "display-#{device}" }
] })
end
if args.include?("screenshot")
FileUtils.mkdir_p(File.dirname(args.last))
File.binwrite(args.last, "new container capture")
end
""
end
capture.define_singleton_method(:dimensions) { |_| Screenshots::DEVICES.fetch(device).fetch(:dimensions) }
containers = [old_container, new_container]
status = Object.new
status.define_singleton_method(:success?) { true }
responses = lambda do |*args|
[args.include?("get_app_container") ? containers.shift : "", status]
end
Open3.stub(:capture2e, responses) do
capture.capture_mobile(device, { "udid" => "fixture", "state" => "Booted" }, "zh-Hans")
end
folder = { "iphone" => "iphone-6.3", "ipad" => "ipad-15" }.fetch(device, device)
destination = File.join(@root, "com.akinokaede.termind/assets/captures/zh-hans/#{folder}/vault.png")
assert_equal "new container capture", File.binread(destination)
assert File.exist?(File.join(control, "ack-#{token}"))
refute Dir.exist?(old_container)
status_bar = commands.find { |args| args.include?("override") }
assert_equal "discharging", status_bar[status_bar.index("--batteryState") + 1]
assert_equal "100", status_bar[status_bar.index("--batteryLevel") + 1]
if device == "iphone-duo-outer"
assert_includes commands.find { |args| args.include?("screenshot") }, "--display=primary"
elsif device == "iphone-duo-inner"
screenshot = commands.find { |args| args.include?("screenshot") }
assert_includes screenshot, "--display-unique-id"
assert_includes screenshot, "display-iphone-duo-inner"
end
FileUtils.remove_entry(new_container)
end
def test_duo_rejects_the_inactive_display_before_starting_capture
capture = runner(:ios, devices: "iphone-duo-inner")
capture.define_singleton_method(:command) do |*args|
JSON.generate("result" => { "displays" => [
{ "active" => true, "nativeSize" => [1398, 2034] },
{ "active" => false, "nativeSize" => [2007, 2853] }
] })
end
error = assert_raises(RuntimeError) { capture.validate_display("iphone-duo-inner", "duo-id") }
assert_includes error.message, "Select Open"
end
def test_failed_scan_never_promotes_a_staged_mac_capture
capture = runner(:mac, scenes: "hosts", locales: "en")
capture.define_singleton_method(:test_configuration) { |*| File.join(@root, "test.xctestrun") }
capture.instance_variable_set(:@scan, ->(_) { raise "scan failed" })
error = assert_raises(RuntimeError) { capture.capture_mac("hosts", "en", "platform=macOS") }
assert_equal "scan failed", error.message
refute Dir.exist?(File.join(@root, "com.akinokaede.termind/assets/captures"))
end
def test_capture_failure_is_reported_even_if_scan_succeeds
capture = runner
attempted = Queue.new
capture.instance_variable_set(:@scan, ->(_) { attempted.pop })
error = assert_raises(RuntimeError) do
capture.capture_during_test({}) do
attempted << true
raise "native capture failed"
end
end
assert_equal "native capture failed", error.message
end
end
require "minitest/mock"
require "fastlane"
require "frameit"
require "zlib"
require_relative "../../fastlane/lib/screenshot_frames"
class ScreenshotFramesTests < Minitest::Test
def setup
@root = Dir.mktmpdir("termind-frames-")
FileUtils.mkdir_p(File.join(@root, "com.akinokaede.termind/assets"))
File.write(File.join(@root, "com.akinokaede.termind/.git"), "gitdir: fixture")
FileUtils.mkdir_p(File.join(@root, "fastlane"))
FileUtils.cp(File.expand_path("../../fastlane/Framefile.json", __dir__), File.join(@root, "fastlane"))
frame_template(File.join(@root, "com.akinokaede.termind/assets/device_templates/iphone-18-pro.png"), 1350, 2760)
@templates = File.join(@root, "templates")
FileUtils.mkdir_p(@templates)
end
def teardown
FileUtils.remove_entry(@root)
end
def png(path, width, height)
chunk = lambda do |name, bytes|
[bytes.bytesize].pack("N") + name + bytes + [Zlib.crc32(name + bytes)].pack("N")
end
data = "\x89PNG\r\n\x1a\n".b + chunk.call("IHDR", [width, height, 8, 2, 0, 0, 0].pack("NNCCCCC"))
data += chunk.call("IDAT", Zlib.deflate(("\0" * (width * 3 + 1)) * height)) + chunk.call("IEND", "")
FileUtils.mkdir_p(File.dirname(path))
File.binwrite(path, data)
end
def frame_template(path, width, height)
FileUtils.mkdir_p(File.dirname(path))
ScreenshotFrames.new.command("magick", "-size", "#{width}x#{height}", "xc:none",
"-fill", "none", "-stroke", "black", "-strokewidth", "40",
"-draw", "roundrectangle 40,40,#{width - 41},#{height - 41},100,100", path)
end
def test_screen_mask_removes_exterior_corners_without_fading_content
runner = ScreenshotFrames.new
template = File.join(@root, "rounded-frame.png")
runner.command("magick", "-size", "120x180", "xc:none", "-fill", "none", "-stroke", "black",
"-strokewidth", "8", "-draw", "roundrectangle 8,8,111,171,25,25",
"-fill", "black", "-stroke", "none", "-draw", "rectangle 50,8,70,28", template)
[0, -90, 90].each do |rotation|
landscape = !rotation.zero?
source = File.join(@root, "screen.png")
runner.command("magick", "-size", landscape ? "160x100" : "100x160", "xc:#327abe", source)
runner.mask_screen(source, template, "+10+10", rotation: rotation)
assert_equal(landscape ? [160, 100] : [100, 160], FastImage.size(source))
center = landscape ? "80,50" : "50,80"
assert_equal "1", runner.command("magick", source, "-format", "%[fx:p{#{center}}.a]", "info:").strip
assert_equal "0", runner.command("magick", source, "-format", "%[fx:p{0,0}.a]", "info:").strip
assert_equal "50,122,190", runner.command("magick", source, "-format",
"%[fx:round(255*p{#{center}}.r)],%[fx:round(255*p{#{center}}.g)],%[fx:round(255*p{#{center}}.b)]", "info:").strip
notch = { 0 => "50,5", -90 => "5,50", 90 => "155,50" }.fetch(rotation)
assert_equal "0", runner.command("magick", source, "-format", "%[fx:p{#{notch}}.a]", "info:").strip
end
end
def test_screen_mask_rejects_an_open_bezel
runner = ScreenshotFrames.new
template = File.join(@root, "open-frame.png")
source = File.join(@root, "screen.png")
runner.command("magick", "-size", "120x180", "xc:none", template)
png(source, 100, 160)
original = Digest::SHA256.file(source).hexdigest
assert_raises(RuntimeError) { runner.mask_screen(source, template, "+10+10") }
assert_equal original, Digest::SHA256.file(source).hexdigest
end
def test_mac_composition_clips_corners_and_preserves_source_and_content
template = File.join(@root, "mac-frame.png")
runner = ScreenshotFrames.new({ mac_frame: template }, root: @root)
runner.command("magick", "-size", "3860x2320", "xc:none", "-fill", "none",
"-stroke", "black", "-strokewidth", "20",
"-draw", "roundrectangle 408,58,3451,2041,100,100", template)
source = File.join(@root, "capture.png")
output = File.join(@root, "composed.png")
runner.command("magick", "-size", "3024x1964", "xc:#327abe", "-fill", "red",
"-draw", "point 1512,982", source)
original = Digest::SHA256.file(source).hexdigest
runner.compose_mac(source, output)
assert_equal [3860, 2320], FastImage.size(output)
# Native screen pixels must land at the aperture offset without resampling.
assert_equal "255,0,0", runner.command("magick", output, "-format",
"%[fx:round(255*p{1930,1050}.r)],%[fx:round(255*p{1930,1050}.g)],%[fx:round(255*p{1930,1050}.b)]", "info:").strip
assert_equal original, Digest::SHA256.file(source).hexdigest
# The old rectangular placement leaked here, outside the curved bezel.
assert_equal "0", runner.command("magick", output, "-format", "%[fx:p{418,68}.a]", "info:").strip
assert_equal "1", runner.command("magick", output, "-format", "%[fx:p{1930,1160}.a]", "info:").strip
assert_equal "50,122,190", runner.command("magick", output, "-format",
"%[fx:round(255*p{1930,1160}.r)],%[fx:round(255*p{1930,1160}.g)],%[fx:round(255*p{1930,1160}.b)]", "info:").strip
end
def test_default_frames_only_include_website_used_scenes
jobs = ScreenshotFrames.new({}, root: @root).plan
assert_equal 7 * Screenshots::LOCALES.size, jobs.size
refute jobs.any? { |job| job[:input].end_with?("/ipad-15/ssh.png", "/iphone-6.3/metrics.png") }
explicit = ScreenshotFrames.new({ devices: "ipad", scenes: "ssh", locales: "en" }, root: @root).plan
assert_equal 1, explicit.size
assert explicit.first[:input].end_with?("/ipad-15/ssh.png")
end
def test_default_frames_fail_before_creating_outputs_when_submodule_is_missing
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en" }, root: @root)
error = assert_raises(RuntimeError) { runner.run { flunk "frameit must not start" } }
assert_includes error.message, "git submodule update --init com.akinokaede.termind"
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_explicit_frame_paths_work_without_submodule_and_keep_source_bytes
template = File.join(@root, "external-template.png")
FileUtils.mv(File.join(@root, "com.akinokaede.termind/assets/device_templates/iphone-18-pro.png"), template)
FileUtils.remove_entry(File.join(@root, "com.akinokaede.termind"))
source = File.join(@root, "review-input/en/iphone-6.3/vault.png")
png(source, 1206, 2622)
original_hash = Digest::SHA256.file(source).hexdigest
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en",
input: "review-input", output: "review-output", iphone_frame: template }, root: @root)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
Frameit::FrameDownloader.stub(:templates_path, @templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
runner.run do |options|
assert_equal "iPhone 18 Pro", options.fetch(:force_device_type)
framefile = JSON.parse(File.read(File.join(options.fetch(:path), "Framefile.json")))
assert_equal({ "offset" => "+72+69", "width" => 1206 }, framefile.fetch("default").fetch("offset"))
png(File.join(options.fetch(:path), "iphone_framed.png"), 1350, 2760)
end
end
end
assert File.exist?(File.join(@root, "review-output/en/iphone-6.3/vault.png"))
assert_equal original_hash, Digest::SHA256.file(source).hexdigest
refute Dir.exist?(File.join(@root, "com.akinokaede.termind"))
end
def test_default_template_works_without_a_local_build_cache
source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png")
png(source, 1206, 2622)
refute Dir.exist?(File.join(@root, ".build"))
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en" }, root: @root)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
Frameit::FrameDownloader.stub(:templates_path, @templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
runner.run do |options|
installed = File.join(@templates, "Apple iPhone 18 Pro Silver.png")
assert_equal Digest::SHA256.file(File.join(@root, "com.akinokaede.termind/assets/device_templates/iphone-18-pro.png")).hexdigest,
Digest::SHA256.file(installed).hexdigest
png(File.join(options.fetch(:path), "iphone_framed.png"), 1350, 2760)
end
end
end
assert_equal [1350, 2760], FastImage.size(File.join(@root, "com.akinokaede.termind/assets/device_frames/en/iphone-6.3/vault.png"))
end
def test_rejects_pro_max_sources_before_calling_frameit
source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png")
png(source, 1320, 2868)
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en" }, root: @root)
called = false
assert_raises(RuntimeError) { runner.run { called = true } }
refute called
refute Dir.exist?(File.join(@root, "com.akinokaede.termind/assets/device_frames"))
end
def test_duo_frames_preserve_native_sources_and_use_each_display_aperture
templates = File.join(@root, "templates")
FileUtils.mkdir_p(templates)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
expected = {
"outer" => [[1398, 2034], [1574, 2194], "+88+80", "iPhone Duo Outer"],
"inner" => [[2853, 2007], [2247, 3093], "+120+120", "iPhone Duo Inner"]
}
expected.each do |display, (native, frame, offset, name)|
device = "iphone-duo-#{display}"
source = File.join(@root, "input/en/#{device}/vault.png")
template = File.join(@root, "#{device}.png")
png(source, *native)
frame_template(template, *frame)
source_hash = Digest::SHA256.file(source).hexdigest
options = { devices: device, scenes: "vault", locales: "en", input: "input", output: "output",
"iphone_duo_#{display}_frame".to_sym => template }
Frameit::FrameDownloader.stub(:templates_path, templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
ScreenshotFrames.new(options, root: @root).run do |configuration|
assert_equal name, configuration.fetch(:force_device_type)
framefile = JSON.parse(File.read(File.join(configuration.fetch(:path), "Framefile.json")))
assert_equal({ "offset" => offset, "width" => native.min }, framefile.fetch("default").fetch("offset"))
assert_equal :landscape_right, configuration.fetch(:force_orientation_block).call(nil) if display == "inner"
png(File.join(configuration.fetch(:path), "#{device}_framed.png"), *(display == "inner" ? frame.reverse : frame))
end
end
end
assert_equal source_hash, Digest::SHA256.file(source).hexdigest
assert_equal(display == "inner" ? frame.reverse : frame, FastImage.size(File.join(@root, "output/en/#{device}/vault.png")))
end
end
def test_missing_duo_template_fails_before_publishing
runner = ScreenshotFrames.new({ devices: "iphone-duo-inner", locales: "en" }, root: @root)
error = assert_raises(RuntimeError) { runner.run { flunk "frameit must not start" } }
assert_includes error.message, "iphone_duo_inner_frame:"
assert_includes error.message, "git submodule update --init com.akinokaede.termind"
refute Dir.exist?(File.join(@root, "com.akinokaede.termind/assets/device_frames"))
end
def test_missing_frameit_output_keeps_all_existing_outputs_and_sources
%w[en zh-hans].each do |locale|
png(File.join(@root, "com.akinokaede.termind/assets/captures/#{locale}/iphone-6.3/vault.png"), 1206, 2622)
end
destination = File.join(@root, "com.akinokaede.termind/assets/device_frames/en/iphone-6.3/vault.png")
FileUtils.mkdir_p(File.dirname(destination))
File.write(destination, "existing frame")
source = File.join(@root, "com.akinokaede.termind/assets/captures/en/iphone-6.3/vault.png")
original_hash = Digest::SHA256.file(source).hexdigest
runner = ScreenshotFrames.new({ devices: "iphone", scenes: "vault", locales: "en,zh-Hans" }, root: @root)
downloader = Object.new
downloader.define_singleton_method(:frames_exist?) { true }
calls = 0
Frameit::FrameDownloader.stub(:templates_path, @templates) do
Frameit::FrameDownloader.stub(:new, downloader) do
assert_raises(RuntimeError) do
runner.run do |options|
calls += 1
png(File.join(options.fetch(:path), "iphone_framed.png"), 1350, 2760) if calls == 1
# frameit can log an error and return successfully without an image.
end
end
end
end
assert_equal 2, calls
assert_equal "existing frame", File.read(destination)
assert_equal original_hash, Digest::SHA256.file(source).hexdigest
refute File.exist?(destination.sub("/en/", "/zh-hans/"))
end
end
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
set -euo pipefail
script_dir=$(cd "$(dirname "$0")" && pwd)
swift_host="$(dirname "$(dirname "$(xcrun --find swift)")")/lib/swift/host"
exec xcrun swift -I "$swift_host" -L "$swift_host" \
-lSwiftParser -lSwiftSyntax -Xlinker -rpath -Xlinker "$swift_host" \
"$script_dir/check-appstrings.swift" "$script_dir/.."
-87
View File
@@ -1,87 +0,0 @@
import Foundation
import SwiftParser
import SwiftSyntax
/// Uses Xcode's parser so comments, interpolation expressions, URLs, symbols,
/// HTTP fields and identifiers cannot be mistaken for UI copy by a text grep.
final class CopyBoundaryVisitor: SyntaxVisitor {
private let path: String
private let locations: SourceLocationConverter
private(set) var violations: [String] = []
private let presentationCalls: Set<String> = [
"Text", "Button", "Label", "TextField", "SecureField", "Section", "LabeledContent", "Picker",
"ContentUnavailableView", "ProgressView", "Toggle", "Menu", "Link", "CommandMenu", "TableColumn", "Window",
"LocalizedStringResource", "LocalizedStringKey", "navigationTitle", "help", "accessibilityLabel",
"accessibilityValue", "accessibilityHint", "alert", "confirmationDialog", "dismissalConfirmationDialog",
"localized", "pageTitle", "feature", "statusCard", "emptyRow", "permissionHeader",
]
init(path: String, tree: SourceFileSyntax) {
self.path = path
locations = SourceLocationConverter(fileName: path, tree: tree)
super.init(viewMode: .sourceAccurate)
}
override func visit(_ node: StringLiteralExprSyntax) -> SyntaxVisitorContinueKind {
let prose = node.segments.compactMap { $0.as(StringSegmentSyntax.self)?.content.text }.joined()
guard prose.contains(where: { $0.isLetter }), isPresentationLiteral(node) else { return .visitChildren }
let line = locations.location(for: node.positionAfterSkippingLeadingTrivia).line
violations.append(
"\(path):\(line): move strings into TermindPresentation/Strings: \(node.trimmedDescription)")
return .visitChildren
}
private func isPresentationLiteral(_ node: StringLiteralExprSyntax) -> Bool {
var ancestor = node.parent
var argument: LabeledExprSyntax?
while let current = ancestor {
if current.is(MacroExpansionExprSyntax.self) { return false }
if let labeled = current.as(LabeledExprSyntax.self), argument == nil { argument = labeled }
if let call = current.as(FunctionCallExprSyntax.self) {
let name =
call.calledExpression.as(MemberAccessExprSyntax.self)?.declName.baseName.text
?? call.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text ?? ""
guard let argument else { return false }
let label = argument.label?.text ?? ""
if name == "String" { return label == "localized" }
if name == "Text", label == "verbatim" { return true }
if ["title", "prompt", "help", "emptyDescription", "actionTitle"].contains(label) { return true }
return label.isEmpty && call.arguments.first?.id == argument.id && presentationCalls.contains(name)
}
if let binding = current.as(PatternBindingSyntax.self), let type = binding.typeAnnotation?.type {
return type.trimmedDescription.contains("LocalizedString")
}
if let function = current.as(FunctionDeclSyntax.self), let type = function.signature.returnClause?.type {
return type.trimmedDescription.contains("LocalizedString")
}
ancestor = current.parent
}
return false
}
}
let repository = URL(fileURLWithPath: CommandLine.arguments[1]).standardizedFileURL
let sharedStrings = repository.appending(path: "Frameworks/TermindPresentation/Strings/AppStrings.swift")
guard FileManager.default.fileExists(atPath: sharedStrings.path) else {
throw CocoaError(.fileReadNoSuchFile)
}
var violations: [String] = []
for app in ["TFM", "TFI"] {
let directory = repository.appending(path: app)
guard let files = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: nil) else {
throw CocoaError(.fileReadNoSuchFile)
}
for case let file as URL in files where file.pathExtension == "swift" {
let source = try String(contentsOf: file, encoding: .utf8)
let tree = Parser.parse(source: source)
let visitor = CopyBoundaryVisitor(path: file.path, tree: tree)
visitor.walk(tree)
violations += visitor.violations
}
}
if !violations.isEmpty {
FileHandle.standardError.write(Data((violations.sorted().joined(separator: "\n") + "\n").utf8))
exit(1)
}
print("TFM and TFI presentation copy uses shared AppStrings resources.")
-45
View File
@@ -1,45 +0,0 @@
[
{ "title": "AlmaLinux", "slug": "almalinux", "ids": ["almalinux", "alma"] },
{ "title": "Alpine Linux", "slug": "alpinelinux", "ids": ["alpine"] },
{ "title": "Arch Linux", "slug": "archlinux", "ids": ["arch"] },
{ "title": "Artix Linux", "slug": "artixlinux", "ids": ["artix"] },
{ "title": "Asahi Linux", "slug": "asahilinux", "ids": ["asahi"] },
{ "title": "CachyOS", "slug": "cachyos", "ids": ["cachyos"] },
{ "title": "CentOS", "slug": "centos", "ids": ["centos"] },
{ "title": "Debian", "slug": "debian", "ids": ["debian"] },
{ "title": "deepin", "slug": "deepin", "ids": ["deepin"] },
{ "title": "Devuan", "slug": "devuan", "ids": ["devuan"] },
{ "title": "elementary", "slug": "elementary", "ids": ["elementary"] },
{ "title": "EndeavourOS", "slug": "endeavouros", "ids": ["endeavouros"] },
{ "title": "Fedora", "slug": "fedora", "ids": ["fedora"] },
{ "title": "Garuda Linux", "slug": "garudalinux", "ids": ["garuda"] },
{ "title": "Gentoo", "slug": "gentoo", "ids": ["gentoo"] },
{ "title": "Kali Linux", "slug": "kalilinux", "ids": ["kali"] },
{ "title": "Kubuntu", "slug": "kubuntu", "ids": ["kubuntu"] },
{ "title": "Linux Mint", "slug": "linuxmint", "ids": ["linuxmint"] },
{ "title": "Lubuntu", "slug": "lubuntu", "ids": ["lubuntu"] },
{ "title": "Manjaro", "slug": "manjaro", "ids": ["manjaro"] },
{ "title": "MX Linux", "slug": "mxlinux", "ids": ["mx"] },
{ "title": "NixOS", "slug": "nixos", "ids": ["nixos"] },
{ "title": "Nobara Linux", "slug": "nobaralinux", "ids": ["nobara"] },
{
"title": "openSUSE",
"slug": "opensuse",
"ids": ["opensuse", "opensuse-leap", "opensuse-tumbleweed", "opensuse-microos"]
},
{ "title": "OpenWrt", "slug": "openwrt", "ids": ["openwrt"] },
{ "title": "Parrot Security", "slug": "parrotsecurity", "ids": ["parrot"] },
{ "title": "Pop!_OS", "slug": "popos", "ids": ["pop", "popos"] },
{ "title": "Qubes OS", "slug": "qubesos", "ids": ["qubes"] },
{ "title": "Raspberry Pi", "slug": "raspberrypi", "ids": ["raspbian", "raspberrypios"] },
{ "title": "Red Hat", "slug": "redhat", "ids": ["rhel", "redhat"] },
{ "title": "Rocky Linux", "slug": "rockylinux", "ids": ["rocky"] },
{ "title": "Slackware", "slug": "slackware", "ids": ["slackware"] },
{ "title": "Solus", "slug": "solus", "ids": ["solus"] },
{ "title": "SUSE", "slug": "suse", "ids": ["suse", "sles", "sled"] },
{ "title": "Ubuntu", "slug": "ubuntu", "ids": ["ubuntu"] },
{ "title": "Ubuntu MATE", "slug": "ubuntumate", "ids": ["ubuntu-mate", "ubuntu-mate-desktop"] },
{ "title": "Void Linux", "slug": "voidlinux", "ids": ["void"] },
{ "title": "Xubuntu", "slug": "xubuntu", "ids": ["xubuntu"] },
{ "title": "Zorin", "slug": "zorin", "ids": ["zorin"] }
]
-41
View File
@@ -1,41 +0,0 @@
import AppKit
import SwiftUI
// Preserve the former native Pro hero in RevenueCat's remote image component.
// Run from the repository root: swift Scripts/render-pro-paywall-icon.swift
@MainActor
func renderIcon(name: String, red: Double, green: Double, blue: Double) throws {
let content = Image(systemName: "sparkles.rectangle.stack.fill")
.font(.system(size: 52, weight: .medium))
.foregroundStyle(.tint)
.tint(Color(red: red, green: green, blue: blue))
.symbolRenderingMode(.hierarchical)
.frame(width: 80, height: 72)
let renderer = ImageRenderer(content: content)
renderer.scale = 4
guard let cgImage = renderer.cgImage,
let png = NSBitmapImageRep(cgImage: cgImage).representation(using: .png, properties: [:])
else { throw CocoaError(.fileWriteUnknown) }
let directory = URL(filePath: FileManager.default.currentDirectoryPath)
.appending(path: "com.akinokaede.termind/design/paywall", directoryHint: .isDirectory)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
try png.write(to: directory.appending(path: name))
}
let assetRoot = URL(filePath: FileManager.default.currentDirectoryPath)
.appending(path: "com.akinokaede.termind", directoryHint: .isDirectory)
guard FileManager.default.fileExists(atPath: assetRoot.appending(path: ".git").path),
FileManager.default.fileExists(atPath: assetRoot.appending(path: "design/brand").path)
else {
throw NSError(
domain: "Termind.AssetWorkflow", code: 1,
userInfo: [
NSLocalizedDescriptionKey:
"Initialize the website and assets first: git submodule update --init com.akinokaede.termind"
])
}
try MainActor.assumeIsolated {
try renderIcon(name: "pro_hero_light.png", red: 0, green: 122.0 / 255, blue: 1)
try renderIcon(name: "pro_hero_dark.png", red: 10.0 / 255, green: 132.0 / 255, blue: 1)
}
-4
View File
@@ -1,4 +0,0 @@
#!/bin/zsh
set -euo pipefail
cd "${0:A:h:h}"
exec bundle exec fastlane mac screenshots "$@"
-4
View File
@@ -1,4 +0,0 @@
#!/bin/zsh
set -euo pipefail
cd "${0:A:h:h}"
exec bundle exec fastlane ios screenshots "$@"
+1 -1
View File
@@ -70,7 +70,7 @@ def main():
simple_source["source"] = "https://github.com/simple-icons/simple-icons"
tabler_source["source"] = "https://github.com/tabler/tabler-icons"
metadata = {entry["title"]: entry for entry in json.loads(read(simple, "data/simple-icons.json"))}
requested = json.loads((ROOT / "Scripts/linux-distribution-icons.json").read_text())
requested = json.loads((ROOT / "Configuration/HostIcons/linux-distribution-icons.json").read_text())
requested.append({"title": "macOS", "slug": "macos", "ids": ["macos"], "id": "macos"})
for title, slug, ids in [
("BSD", "bsd", ["bsd", "dragonfly"]),
@@ -1,9 +0,0 @@
#!/bin/sh
set -eu
# Compatibility entry point; generation now includes macOS and Windows artwork.
if [ "$#" -ne 1 ]; then
echo "usage: $0 SIMPLE_ICONS_VERSION" >&2
exit 64
fi
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
exec python3 "$script_dir/update-host-icons.py" --simple-icons-version "$1"