test: 增加测试

This commit is contained in:
2026-04-25 01:38:33 +08:00
parent 6ed1455111
commit 2c9e854507
23 changed files with 1573 additions and 1933 deletions

54
tests/conftest.py Normal file
View File

@@ -0,0 +1,54 @@
"""
Pytest shared fixtures for HeurAMS test suite.
Provides:
- timer_config: A ConfigDict with deterministic timer overrides for reproducible tests.
- timer_context: A ConfigContext that applies timer_config for the duration of a test.
- sample_algodata_sm2: A fresh SM-2 algodata dict.
- sample_algodata_nsp0: A fresh NSP-0 algodata dict.
"""
import pathlib
from copy import deepcopy
import pytest
from heurams.context import ConfigContext, config_var, workdir
from heurams.kernel.algorithms import algorithms, nsp0
from heurams.services.config import ConfigDict
@pytest.fixture
def timer_config():
"""A ConfigDict with deterministic timer overrides (daystamp=20000, timestamp=1e9).
This allows reproducible algorithm tests without relying on wall-clock time.
"""
# Use the real config path as base, then overlay timer overrides
config = ConfigDict(workdir / "data" / "config")
# Override timer values in-place via the nested ConfigDict
timer_cfg = config["services"]["timer"]
timer_cfg["daystamp_override"] = 20000
timer_cfg["timestamp_override"] = 1000000000.0
return config
@pytest.fixture
def timer_context(timer_config):
"""Context manager fixture that applies the timer overrides."""
with ConfigContext(timer_config):
yield
@pytest.fixture
def sample_algodata_sm2():
"""A fresh SM-2 algodata dict (pre-activation)."""
algo = algorithms["SM-2"]
return {algo.algo_name: deepcopy(algo.defaults)}
@pytest.fixture
def sample_algodata_nsp0():
"""A fresh NSP-0 algodata dict (pre-activation)."""
algo = algorithms["NSP-0"]
return {algo.algo_name: deepcopy(algo.defaults)}

View File

@@ -0,0 +1,58 @@
"""Tests for heurams.kernel.algorithms.base.BaseAlgorithm"""
from copy import deepcopy
import pytest
from heurams.kernel.algorithms import BaseAlgorithm
from heurams.services import timer
class TestBaseAlgorithmDefaults:
def test_defaults_have_required_keys(self):
required = {
"real_rept",
"rept",
"interval",
"last_date",
"next_date",
"is_activated",
"last_modify",
}
assert required.issubset(BaseAlgorithm.defaults.keys())
def test_defaults_last_modify_is_reasonable(self):
# defaults is evaluated at module import, not test time
ts = BaseAlgorithm.defaults["last_modify"]
assert isinstance(ts, float)
assert ts > 1e9 # reasonable UNIX timestamp
class TestBaseAlgorithmMethods:
def test_revisor_does_nothing(self):
d = {"SM-2": {"rept": 0}}
BaseAlgorithm.revisor(d, feedback=5)
# Base.revisor is a no-op — dict unchanged
assert d["SM-2"]["rept"] == 0
def test_is_due_returns_one(self):
assert BaseAlgorithm.is_due({}) == 1
def test_get_rating_returns_empty(self):
assert BaseAlgorithm.get_rating({}) == ""
def test_nextdate_returns_negative_one(self):
assert BaseAlgorithm.nextdate({}) == -1
class TestBaseAlgorithmIntegrity:
def test_check_integrity_valid(self, sample_algodata_sm2):
# BaseAlgorithm.algo_name is "BaseAlgorithm", not "SM-2"
data = {"BaseAlgorithm": sample_algodata_sm2["SM-2"]}
assert BaseAlgorithm.check_integrity(data) == 1
def test_check_integrity_invalid(self):
assert BaseAlgorithm.check_integrity({"SM-2": {}}) == 0
def test_check_integrity_missing_key(self):
assert BaseAlgorithm.check_integrity({}) == 0

150
tests/test_electron.py Normal file
View File

@@ -0,0 +1,150 @@
"""Tests for heurams.kernel.particles.electron.Electron"""
from copy import deepcopy
import pytest
from heurams.kernel.algorithms import algorithms
from heurams.kernel.particles.electron import Electron
from heurams.services import timer
class TestElectronInit:
def test_default_algo_is_sm2(self, timer_context):
e = Electron("test-id", {})
assert e.algoname == "SM-2"
assert e.ident == "test-id"
def test_specific_algo(self, timer_context):
e = Electron("test-id", {}, algo_name="NSP-0")
assert e.algoname == "NSP-0"
def test_integrity_check_fills_defaults(self, timer_context):
e = Electron("test-id", {})
assert "SM-2" in e.algodata
assert e.algodata["SM-2"]["efactor"] == 2.5
def test_existing_data_preserved(self, timer_context):
data = {"SM-2": {"efactor": 1.5, "rept": 3, "real_rept": 5, "interval": 10,
"last_date": 100, "next_date": 200, "is_activated": 1,
"last_modify": 1e9}}
e = Electron("test-id", data)
assert e.algodata["SM-2"]["efactor"] == 1.5
assert e.algodata["SM-2"]["rept"] == 3
class TestElectronActivation:
def test_activate_sets_flag(self, timer_context):
e = Electron("test-id", {})
assert e.is_activated() == 0
e.activate()
assert e.is_activated() == 1
def test_is_due_requires_activation(self, timer_context):
e = Electron("test-id", {})
e.algodata["SM-2"]["next_date"] = 0 # past
assert e.is_due() == 0 # not activated
e.activate()
assert e.is_due() == 1
def test_is_due_returns_false_when_not_due(self, timer_context):
e = Electron("test-id", {})
e.activate()
e.algodata["SM-2"]["next_date"] = 999999
assert e.is_due() is False
class TestElectronModify:
def test_modify_valid_key(self, timer_context):
e = Electron("test-id", {})
e.modify("efactor", 3.0)
assert e.algodata["SM-2"]["efactor"] == 3.0
def test_modify_invalid_key_raises(self, timer_context):
e = Electron("test-id", {})
with pytest.raises(AttributeError):
e.modify("nonexistent", 42)
class TestElectronRevisor:
def test_revisor_delegates_to_algo(self, timer_context):
e = Electron("test-id", {})
e.activate()
e.algodata["SM-2"]["next_date"] = 0
assert e.is_due() == 1
e.revisor(quality=5)
# After good review, interval > 0
assert e.algodata["SM-2"]["interval"] >= 1
def test_revisor_nsp0(self, timer_context):
e = Electron("test-id", {}, algo_name="NSP-0")
e.activate()
e.algodata["NSP-0"]["next_date"] = 0
assert e.is_due() == 1
e.revisor(quality=3) # bad feedback
assert e.algodata["NSP-0"]["interval"] == 1
class TestElectronProperties:
def test_rept(self, timer_context):
e = Electron("test-id", {})
assert e.rept() == 0
def test_rept_real(self, timer_context):
e = Electron("test-id", {})
assert e.rept(real_rept=True) == 0
def test_get_rating(self, timer_context):
e = Electron("test-id", {})
rating = e.get_rating()
assert isinstance(rating, str)
def test_nextdate_returns_int(self, timer_context):
e = Electron("test-id", {})
nd = e.nextdate()
assert isinstance(nd, int)
def test_hash(self, timer_context):
e = Electron("test-id", {})
assert hash(e) == hash("test-id")
def test_len(self, timer_context):
e = Electron("test-id", {})
assert len(e) == len(algorithms["SM-2"].defaults)
class TestElectronGetSetItem:
def test_getitem_ident(self, timer_context):
e = Electron("test-id", {})
assert e["ident"] == "test-id"
def test_getitem_algo_key(self, timer_context):
e = Electron("test-id", {})
assert e["efactor"] == 2.5
def test_getitem_missing_key_raises(self, timer_context):
e = Electron("test-id", {})
with pytest.raises(KeyError):
_ = e["nonexistent"]
def test_setitem_valid_key(self, timer_context):
e = Electron("test-id", {})
e["efactor"] = 3.5
assert e["efactor"] == 3.5
def test_setitem_ident_raises(self, timer_context):
e = Electron("test-id", {})
with pytest.raises(AttributeError):
e["ident"] = "new-id"
class TestElectronFromData:
def test_from_data_creates_electron(self, timer_context):
data = {"SM-2": {}}
e = Electron.from_data(("my-ident", data), algo_name="SM-2")
assert e.ident == "my-ident"
assert e.algoname == "SM-2"
assert "SM-2" in e.algodata

72
tests/test_epath.py Normal file
View File

@@ -0,0 +1,72 @@
"""Tests for heurams.services.epath"""
from heurams.services.epath import epath
class TestEpathRead:
def test_empty_path_returns_self(self):
d = {"a": 1}
assert epath(d, "") is d
def test_simple_key(self):
d = {"a": 1}
assert epath(d, "a") == 1
def test_nested_key(self):
d = {"a": {"b": {"c": 42}}}
assert epath(d, "a.b.c") == 42
def test_missing_key_returns_default(self):
d = {"a": 1}
assert epath(d, "b", default=None) is None
def test_missing_key_no_default(self):
d = {"a": 1}
assert epath(d, "b") is None
def test_list_index_access(self):
d = {"items": [10, 20, 30]}
assert epath(d, "items.[1]") == 20
def test_leading_dot_stripped(self):
d = {"a": 1}
assert epath(d, ".a") == 1
def test_trailing_dot_stripped(self):
d = {"a": 1}
assert epath(d, "a.") == 1
class TestEpathParents:
def test_parents_creates_missing_dict_keys(self):
d = {}
result = epath(d, "a.b.c", parents=True, default=None)
# parents=True creates all intermediate keys including the leaf
assert result == {}
assert d == {"a": {"b": {"c": {}}}}
class TestEpathModify:
def test_modify_dict_key(self):
d = {"a": 1}
result = epath(d, "a", enable_modify=True, new_value=99)
assert result == 99
assert d["a"] == 99
def test_modify_nested_key(self):
d = {"a": {"b": 2}}
epath(d, "a.b", enable_modify=True, new_value=42)
assert d["a"]["b"] == 42
def test_modify_list_index(self):
d = {"items": [10, 20]}
epath(d, "items.[0]", enable_modify=True, new_value=99)
assert d["items"][0] == 99
def test_modify_list_index_with_parents(self):
d = {"items": []}
result = epath(
d, "items.[3]", enable_modify=True, new_value=42, parents=True
)
assert result == 42
assert d["items"] == [None, None, None, 42]

47
tests/test_evalizor.py Normal file
View File

@@ -0,0 +1,47 @@
"""Tests for heurams.kernel.auxiliary.evalizor.Evalizer"""
from heurams.kernel.auxiliary.evalizor import Evalizer
class TestEvalizer:
def test_noop_on_plain_string(self):
e = Evalizer({"x": 42})
assert e("hello") == "hello"
def test_eval_expression(self):
e = Evalizer({"x": 42})
assert e("eval: x") == 42
def test_eval_arithmetic(self):
e = Evalizer({"a": 10, "b": 20})
assert e("eval: a + b") == 30
def test_traverses_dict(self):
e = Evalizer({"val": 99})
data = {"key_a": "plain", "key_b": "eval: val + 1"}
result = e(data)
assert result == {"key_a": "plain", "key_b": 100}
def test_traverses_list(self):
e = Evalizer({"val": 5})
data = ["eval: val", "plain", "eval: val * 2"]
result = e(data)
assert result == [5, "plain", 10]
def test_traverses_nested(self):
e = Evalizer({"val": 3})
data = {"outer": {"inner": "eval: val ** 2"}}
result = e(data)
assert result == {"outer": {"inner": 9}}
def test_traverses_tuple(self):
e = Evalizer({"val": 7})
data = ("eval: val", "other")
result = e(data)
assert result == (7, "other")
def test_non_string_passthrough(self):
e = Evalizer({})
assert e(42) == 42
assert e(None) is None
assert e([1, 2, 3]) == [1, 2, 3]

25
tests/test_hasher.py Normal file
View File

@@ -0,0 +1,25 @@
"""Tests for heurams.services.hasher"""
from heurams.services.hasher import get_md5, hash
class TestGetMD5:
def test_known_value(self):
# MD5 of "hello" is known
assert get_md5("hello") == "5d41402abc4b2a76b9719d911017c592"
def test_empty_string(self):
assert get_md5("") == "d41d8cd98f00b204e9800998ecf8427e"
def test_unicode(self):
result = get_md5("中文测试")
assert isinstance(result, str)
assert len(result) == 32
def test_different_inputs_differ(self):
assert get_md5("abc") != get_md5("abcd")
class TestHash:
def test_hash_delegates_to_md5(self):
assert hash("hello") == get_md5("hello")

198
tests/test_lict.py Normal file
View File

@@ -0,0 +1,198 @@
"""Tests for heurams.kernel.auxiliary.lict.Lict"""
import pytest
from heurams.kernel.auxiliary.lict import Lict
class TestLictInit:
def test_empty(self):
l = Lict()
assert len(l) == 0
assert list(l) == []
def test_from_list(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
assert l["a"] == 1
assert l["b"] == 2
assert len(l) == 2
def test_from_dict(self):
l = Lict(initdict={"x": 10, "y": 20})
assert l["x"] == 10
assert l["y"] == 20
assert len(l) == 2
class TestLictListInterface:
def test_list_getitem(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
assert l[0] == ("a", 1)
assert l[1] == ("b", 2)
def test_list_setitem(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
l[0] = ("c", 3)
assert l["c"] == 3
assert l[0] == ("c", 3)
def test_list_delitem(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
del l[0]
assert "a" not in l
assert len(l) == 1
def test_append(self):
l = Lict()
l.append(("k", "v"))
assert l["k"] == "v"
assert l[0] == ("k", "v")
def test_insert(self):
l = Lict(initlist=[("a", 1), ("c", 3)])
l.insert(1, ("b", 2))
assert l[1] == ("b", 2)
assert l["b"] == 2
assert len(l) == 3
def test_pop(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
item = l.pop()
assert item == ("b", 2)
assert "b" not in l
def test_remove_by_key(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
l.remove("a")
assert "a" not in l
assert len(l) == 1
def test_remove_by_tuple(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
l.remove(("a", 1))
assert "a" not in l
def test_clear(self):
l = Lict(initlist=[("a", 1)])
l.clear()
assert len(l) == 0
assert list(l) == []
class TestLictDictInterface:
def test_dict_getitem(self):
l = Lict(initlist=[("a", 1)])
assert l["a"] == 1
def test_dict_setitem(self):
l = Lict()
l["k"] = "v"
assert l["k"] == "v"
# dict set marks list dirty — sync on access
assert l[0] == ("k", "v")
def test_dict_delitem(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
del l["a"]
assert "a" not in l
assert len(l) == 1
def test_keys(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
assert set(l.keys()) == {"a", "b"}
def test_values(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
assert set(l.values()) == {1, 2}
def test_items(self):
l = Lict(initlist=[("a", 1), ("b", 2)])
assert set(l.items()) == {("a", 1), ("b", 2)}
def test_get_itemic_unit(self):
l = Lict(initlist=[("a", 1)])
assert l.get_itemic_unit("a") == ("a", 1)
class TestLictSync:
def test_dict_to_list_sync(self):
"""After dict modification, list access triggers sync."""
l = Lict(initdict={"a": 1})
assert l[0] == ("a", 1)
def test_list_to_dict_sync(self):
"""After list modification, dict access triggers sync."""
l = Lict(initlist=[("a", 1)])
assert l["a"] == 1
def test_append_list_maintained(self):
l = Lict()
l.append(("x", 100))
l.append(("y", 200))
# List order preserved
assert list(l) == [("x", 100), ("y", 200)]
class TestLictEdgeCases:
def test_append_non_tuple_raises(self):
l = Lict()
with pytest.raises(NotImplementedError):
l.append("not_a_tuple") # type: ignore
def test_append_bad_tuple_raises(self):
l = Lict()
with pytest.raises(NotImplementedError):
l.append((1, 2, 3)) # type: ignore
def test_contains_by_key(self):
l = Lict(initlist=[("a", 1)])
assert "a" in l
def test_contains_by_value(self):
l = Lict(initlist=[("a", 1)])
assert 1 in l
def test_contains_by_tuple(self):
l = Lict(initlist=[("a", 1)])
assert ("a", 1) in l
def test_not_contains(self):
l = Lict(initlist=[("a", 1)])
assert "z" not in l
def test_forced_order(self):
l = Lict(initlist=[("b", 2), ("a", 1)], forced_order=True)
assert l[0] == ("a", 1)
assert l[1] == ("b", 2)
def test_append_if_not_exists(self):
l = Lict()
l.append_if_it_doesnt_exist_before(("k", "v"))
assert l["k"] == "v"
l.append_if_it_doesnt_exist_before(("k", "v"))
assert len(l) == 1
def test_keys_equal_with(self):
a = Lict(initlist=[("x", 1), ("y", 2)])
b = Lict(initlist=[("y", 3), ("x", 4)])
assert a.keys_equal_with(b)
def test_index_raises(self):
l = Lict()
with pytest.raises(NotImplementedError):
l.index()
def test_extend_raises(self):
l = Lict()
with pytest.raises(NotImplementedError):
l.extend()
def test_sort_raises(self):
l = Lict()
with pytest.raises(NotImplementedError):
l.sort()
def test_reverse_raises(self):
l = Lict()
with pytest.raises(NotImplementedError):
l.reverse()

70
tests/test_nsp0.py Normal file
View File

@@ -0,0 +1,70 @@
"""Tests for heurams.kernel.algorithms.nsp0.NSP0Algorithm"""
from copy import deepcopy
import pytest
from heurams.kernel.algorithms import algorithms
from heurams.services import timer
@pytest.fixture
def algo():
return algorithms["NSP-0"]
@pytest.fixture
def algodata(sample_algodata_nsp0):
return sample_algodata_nsp0
class TestNSP0Defaults:
def test_defaults_have_important(self, algo):
assert algo.defaults["important"] == 0
def test_algo_name(self, algo):
assert algo.algo_name == "NSP-0"
class TestNSP0Revisor:
def test_negative_one_skip(self, algo, algodata):
d = deepcopy(algodata)
algo.revisor(d, feedback=-1)
assert d == algodata
def test_feedback_three_or_less_sets_interval_one(self, algo, algodata):
for fb in (0, 1, 2, 3):
d = deepcopy(algodata)
algo.revisor(d, feedback=fb)
assert d["NSP-0"]["interval"] == 1
assert d["NSP-0"]["important"] == 1
def test_feedback_greater_than_three_sets_infinite_interval(self, algo, algodata):
for fb in (4, 5):
d = deepcopy(algodata)
algo.revisor(d, feedback=fb)
assert d["NSP-0"]["interval"] == float("inf")
assert d["NSP-0"]["important"] == 0
def test_revisor_updates_dates(self, algo, algodata, timer_context):
d = deepcopy(algodata)
algo.revisor(d, feedback=3)
assert d["NSP-0"]["last_date"] == timer.get_daystamp()
assert d["NSP-0"]["next_date"] == timer.get_daystamp() + 1
class TestNSP0IsDue:
def test_due_when_past(self, algo, algodata, timer_context):
d = deepcopy(algodata)
d["NSP-0"]["next_date"] = 100
assert algo.is_due(d) is True
def test_not_due_when_future(self, algo, algodata, timer_context):
d = deepcopy(algodata)
d["NSP-0"]["next_date"] = 999999
assert algo.is_due(d) is False
def test_nextdate_returns_stored(self, algo, algodata):
d = deepcopy(algodata)
d["NSP-0"]["next_date"] = 42
assert algo.nextdate(d) == 42

123
tests/test_sm2.py Normal file
View File

@@ -0,0 +1,123 @@
"""Tests for heurams.kernel.algorithms.sm2.SM2Algorithm"""
from copy import deepcopy
import pytest
from heurams.kernel.algorithms import algorithms
from heurams.services import timer
@pytest.fixture
def algo():
return algorithms["SM-2"]
@pytest.fixture
def algodata(sample_algodata_sm2):
return sample_algodata_sm2
class TestSM2Defaults:
def test_defaults_have_efactor(self, algo):
assert algo.defaults["efactor"] == 2.5
def test_algo_name(self, algo):
assert algo.algo_name == "SM-2"
class TestSM2Revisor:
def test_feedback_negative_one_skips(self, algo, algodata):
"""feedback == -1 should be a no-op."""
d = deepcopy(algodata)
algo.revisor(d, feedback=-1)
assert d == algodata # unchanged
def test_good_feedback_increases_efactor(self, algo, algodata):
d = deepcopy(algodata)
ef_before = d["SM-2"]["efactor"]
algo.revisor(d, feedback=5)
assert d["SM-2"]["efactor"] > ef_before
def test_bad_feedback_resets_rept(self, algo, algodata):
d = deepcopy(algodata)
d["SM-2"]["rept"] = 5
algo.revisor(d, feedback=2)
assert d["SM-2"]["rept"] == 0
assert d["SM-2"]["interval"] == 1
def test_efactor_minimum_floor(self, algo, algodata):
d = deepcopy(algodata)
d["SM-2"]["efactor"] = 0.5
algo.revisor(d, feedback=2)
assert d["SM-2"]["efactor"] >= 1.3
def test_rept_increments_on_good_feedback(self, algo, algodata):
d = deepcopy(algodata)
algo.revisor(d, feedback=4)
assert d["SM-2"]["rept"] == 1
def test_new_activation_resets_state(self, algo, algodata):
d = deepcopy(algodata)
d["SM-2"]["rept"] = 10
d["SM-2"]["efactor"] = 3.0
algo.revisor(d, feedback=5, is_new_activation=True)
assert d["SM-2"]["rept"] == 0
assert d["SM-2"]["efactor"] == 2.5
def test_interval_at_rept_zero(self, algo, algodata):
d = deepcopy(algodata)
algo.revisor(d, feedback=2)
assert d["SM-2"]["interval"] == 1
def test_interval_at_rept_one(self, algo, algodata):
d = deepcopy(algodata)
# rept=0 + feedback>=3 -> rept becomes 1 -> interval=6
algo.revisor(d, feedback=5)
assert d["SM-2"]["interval"] == 6
def test_interval_for_rept_gt_one(self, algo, algodata):
d = deepcopy(algodata)
d["SM-2"]["rept"] = 2
d["SM-2"]["interval"] = 6
d["SM-2"]["efactor"] = 2.0
algo.revisor(d, feedback=5)
# efactor 2.0 + 0.1(feedback=5) = 2.1; interval = round(6 * 2.1) = 13
assert d["SM-2"]["interval"] == 13
def test_real_rept_always_increments(self, algo, algodata):
d = deepcopy(algodata)
algo.revisor(d, feedback=5)
assert d["SM-2"]["real_rept"] == 1
algo.revisor(d, feedback=0)
assert d["SM-2"]["real_rept"] == 2
class TestSM2DueDate:
def test_is_due_when_past(self, algo, algodata, timer_context):
d = deepcopy(algodata)
d["SM-2"]["next_date"] = 100 # far in the past
assert algo.is_due(d) is True
def test_not_due_when_future(self, algo, algodata, timer_context):
d = deepcopy(algodata)
d["SM-2"]["next_date"] = 999999 # far in the future
assert algo.is_due(d) is False
def test_nextdate_returns_stored(self, algo, algodata):
d = deepcopy(algodata)
d["SM-2"]["next_date"] = 12345
assert algo.nextdate(d) == 12345
def test_revisor_updates_dates(self, algo, algodata, timer_context):
d = deepcopy(algodata)
algo.revisor(d, feedback=5)
assert d["SM-2"]["last_date"] == timer.get_daystamp()
assert d["SM-2"]["next_date"] > timer.get_daystamp()
class TestSM2Rating:
def test_get_rating_returns_efactor(self, algo, algodata):
d = deepcopy(algodata)
d["SM-2"]["efactor"] = 2.5
assert algo.get_rating(d) == "2.5"

35
tests/test_textproc.py Normal file
View File

@@ -0,0 +1,35 @@
"""Tests for heurams.services.textproc"""
from heurams.services.textproc import domize, truncate, undomize
class TestTruncate:
def test_short_string_unchanged(self):
assert truncate("ab") == "ab"
def test_three_char_unchanged(self):
assert truncate("abc") == "abc"
def test_longer_string_truncated(self):
assert truncate("abcd") == "abc>"
def test_empty_string(self):
assert truncate("") == ""
class TestDomizeUndomize:
def test_domize_replaces_dot(self):
assert domize("a.b.c") == "a--DOT--b--DOT--c"
def test_domize_no_dot(self):
assert domize("abc") == "abc"
def test_undomize_restores_dot(self):
assert undomize("a--DOT--b") == "a.b"
def test_undomize_no_marker(self):
assert undomize("abc") == "abc"
def test_roundtrip(self):
original = "config.key.subkey"
assert undomize(domize(original)) == original