import unittest
from hypothesis import given, strategies as st, assume, settings, HealthCheck
import shlesha
from vidyut.lipi import transliterate, Scheme
@st.composite
def sanskrit_text(draw, script="iast", max_length=20):
if script == "iast":
vowels = ["a", "ā", "i", "ī", "u", "ū", "ṛ", "ṝ", "ḷ", "ḹ", "e", "ai", "o", "au"]
consonants = ["k", "kh", "g", "gh", "ṅ", "c", "ch", "j", "jh", "ñ",
"ṭ", "ṭh", "ḍ", "ḍh", "ṇ", "t", "th", "d", "dh", "n",
"p", "ph", "b", "bh", "m", "y", "r", "l", "v",
"ś", "ṣ", "s", "h"]
marks = ["ṃ", "ḥ"]
specials = ["kṣ", "jñ"]
chars = vowels + consonants + marks + specials
elif script == "slp1":
vowels = ["a", "A", "i", "I", "u", "U", "f", "F", "x", "X", "e", "E", "o", "O"]
consonants = ["k", "K", "g", "G", "N", "c", "C", "j", "J", "Y",
"w", "W", "q", "Q", "R", "t", "T", "d", "D", "n",
"p", "P", "b", "B", "m", "y", "r", "l", "v",
"S", "z", "s", "h"]
marks = ["M", "H"]
specials = ["kz", "jY"]
chars = vowels + consonants + marks + specials
elif script == "harvard_kyoto":
vowels = ["a", "A", "i", "I", "u", "U", "R", "RR", "lR", "lRR", "e", "ai", "o", "au"]
consonants = ["k", "kh", "g", "gh", "G", "c", "ch", "j", "jh", "J",
"T", "Th", "D", "Dh", "N", "t", "th", "d", "dh", "n",
"p", "ph", "b", "bh", "m", "y", "r", "l", "v",
"z", "S", "s", "h"]
marks = ["M", "H"]
specials = ["kS", "jJ"]
chars = vowels + consonants + marks + specials
else:
chars = ["a", "i", "u", "k", "t", "m", "n", "r", "s"]
length = draw(st.integers(min_value=1, max_value=max_length))
return "".join(draw(st.lists(st.sampled_from(chars), min_size=1, max_size=length)))
class PropertyBasedTests(unittest.TestCase):
@given(sanskrit_text(script="iast"))
@settings(max_examples=200, suppress_health_check=[HealthCheck.too_slow])
def test_transliteration_is_deterministic(self, text):
assume(len(text) > 0)
script_pairs = [
("iast", "slp1"),
("iast", "devanagari"),
("iast", "iso"),
("slp1", "iast"),
("slp1", "devanagari"),
]
for source, target in script_pairs:
try:
result1 = shlesha.transliterate(text, source, target)
result2 = shlesha.transliterate(text, source, target)
result3 = shlesha.transliterate(text, source, target)
self.assertEqual(result1, result2,
f"Non-deterministic result for {source}→{target}: '{text}' gave different outputs")
self.assertEqual(result1, result3,
f"Non-deterministic result for {source}→{target}: '{text}' gave different outputs")
except Exception as e:
try:
shlesha.transliterate(text, source, target)
self.fail(f"Inconsistent error behavior for {source}→{target}: '{text}'")
except Exception:
pass
@given(sanskrit_text(script="iast"))
@settings(max_examples=100)
def test_identity_conversions(self, text):
assume(len(text) > 0)
scripts = ["iast", "slp1", "devanagari", "telugu", "iso"]
for script in scripts:
try:
result = shlesha.transliterate(text, script, script)
self.assertEqual(result, text,
f"Identity conversion failed for {script}: '{text}' → '{result}'")
except Exception:
pass
@given(sanskrit_text(script="iast"))
@settings(max_examples=50)
def test_round_trip_conversions(self, text):
assume(len(text) > 0 and len(text) < 15)
round_trip_pairs = [
("iast", "slp1"),
("iast", "iso"),
("iast", "devanagari"),
("slp1", "devanagari"),
]
for script_a, script_b in round_trip_pairs:
try:
intermediate = shlesha.transliterate(text, script_a, script_b)
back_to_original = shlesha.transliterate(intermediate, script_b, script_a)
self.assertEqual(back_to_original, text,
f"Round-trip failed {script_a}→{script_b}→{script_a}: "
f"'{text}' → '{intermediate}' → '{back_to_original}'")
except Exception as e:
print(f"Round-trip conversion failed for '{text}' ({script_a}↔{script_b}): {e}")
@given(sanskrit_text(script="iast"))
@settings(max_examples=100)
def test_output_length_bounds(self, text):
assume(len(text) > 0)
script_pairs = [
("iast", "slp1"),
("iast", "devanagari"),
("slp1", "iast"),
("iast", "iso"),
]
for source, target in script_pairs:
try:
result = shlesha.transliterate(text, source, target)
if len(text) > 0:
self.assertGreater(len(result), 0,
f"Empty output for non-empty input: {source}→{target} '{text}' → '{result}'")
max_expansion = 10 self.assertLessEqual(len(result), len(text) * max_expansion,
f"Excessive expansion {source}→{target}: '{text}' ({len(text)}) → '{result}' ({len(result)})")
except Exception:
pass
@given(sanskrit_text(script="iast"))
@settings(max_examples=50)
def test_character_preservation(self, text):
assume(len(text) > 0)
ascii_chars = "0123456789.,;:!?-()[]{}'\""
for char in ascii_chars:
test_text = text + char
try:
iast_to_slp1 = shlesha.transliterate(test_text, "iast", "slp1")
self.assertIn(char, iast_to_slp1,
f"ASCII character '{char}' not preserved in IAST→SLP1: '{test_text}' → '{iast_to_slp1}'")
slp1_to_iast = shlesha.transliterate(test_text, "slp1", "iast")
self.assertIn(char, slp1_to_iast,
f"ASCII character '{char}' not preserved in SLP1→IAST: '{test_text}' → '{slp1_to_iast}'")
except Exception:
pass
@given(sanskrit_text(script="iast"), sanskrit_text(script="iast"))
@settings(max_examples=50)
def test_concatenation_property(self, text1, text2):
assume(len(text1) > 0 and len(text2) > 0)
combined_text = text1 + text2
roman_pairs = [
("iast", "slp1"),
("slp1", "iast"),
("iast", "iso"),
("iso", "iast"),
]
for source, target in roman_pairs:
try:
combined_result = shlesha.transliterate(combined_text, source, target)
part1_result = shlesha.transliterate(text1, source, target)
part2_result = shlesha.transliterate(text2, source, target)
parts_combined = part1_result + part2_result
self.assertEqual(combined_result, parts_combined,
f"Concatenation property failed {source}→{target}: "
f"'{combined_text}' → '{combined_result}' vs '{parts_combined}'")
except Exception:
pass
@given(st.lists(st.sampled_from(["a", "ā", "i", "ī", "u", "ū", "k", "t", "m", "n"]), min_size=1, max_size=10))
@settings(max_examples=100)
def test_monotonic_character_mapping(self, chars):
for char in chars:
try:
iast_to_slp1_single = shlesha.transliterate(char, "iast", "slp1")
longer_text = "a" + char + "m"
longer_result = shlesha.transliterate(longer_text, "iast", "slp1")
if len(iast_to_slp1_single) == 1 and iast_to_slp1_single.isalnum():
self.assertIn(iast_to_slp1_single, longer_result,
f"Inconsistent mapping for '{char}': single='{iast_to_slp1_single}' not in longer='{longer_result}'")
except Exception:
pass
@given(sanskrit_text(script="iast"))
@settings(max_examples=50)
def test_vidyut_consistency(self, text):
assume(len(text) > 0 and len(text) < 10)
test_cases = [
("iast", "slp1", Scheme.Iast, Scheme.Slp1),
("iast", "devanagari", Scheme.Iast, Scheme.Devanagari),
("slp1", "devanagari", Scheme.Slp1, Scheme.Devanagari),
]
for shlesha_source, shlesha_target, vidyut_source, vidyut_target in test_cases:
try:
shlesha_result = shlesha.transliterate(text, shlesha_source, shlesha_target)
vidyut_result = transliterate(text, vidyut_source, vidyut_target)
self.assertEqual(shlesha_result, vidyut_result,
f"Shlesha/Vidyut mismatch {shlesha_source}→{shlesha_target}: "
f"'{text}' → Shlesha: '{shlesha_result}', Vidyut: '{vidyut_result}'")
except Exception as e:
print(f"Consistency test failed for '{text}': {e}")
@given(st.text(min_size=1, max_size=5))
@settings(max_examples=100)
def test_error_handling(self, text):
invalid_scripts = ["nonexistent", "", "invalid123", "IAST", "SLP1"]
valid_scripts = ["iast", "slp1", "devanagari", "telugu", "iso"]
for invalid_script in invalid_scripts:
for valid_script in valid_scripts:
with self.assertRaises(Exception):
shlesha.transliterate(text, invalid_script, valid_script)
with self.assertRaises(Exception):
shlesha.transliterate(text, valid_script, invalid_script)
def test_supported_scripts_property(self):
scripts = shlesha.get_supported_scripts()
self.assertIsInstance(scripts, list)
self.assertGreater(len(scripts), 0)
for script in scripts:
self.assertIsInstance(script, str)
self.assertGreater(len(script), 0)
expected_scripts = ["iast", "slp1", "devanagari", "telugu"]
for expected in expected_scripts:
self.assertIn(expected, scripts, f"Expected script '{expected}' not found in supported scripts")
@given(st.integers(min_value=0, max_value=1000))
def test_empty_and_numeric_inputs(self, number):
for script in ["iast", "slp1", "devanagari"]:
result = shlesha.transliterate("", script, script)
self.assertEqual(result, "", f"Empty string conversion failed for {script}")
numeric_text = str(number)
try:
result = shlesha.transliterate(numeric_text, "iast", "slp1")
self.assertIsInstance(result, str)
except Exception:
pass
if __name__ == '__main__':
unittest.main(verbosity=2, buffer=True)