test: 增加测试
This commit is contained in:
54
tests/conftest.py
Normal file
54
tests/conftest.py
Normal 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)}
|
||||
58
tests/test_base_algorithm.py
Normal file
58
tests/test_base_algorithm.py
Normal 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
150
tests/test_electron.py
Normal 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
72
tests/test_epath.py
Normal 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
47
tests/test_evalizor.py
Normal 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
25
tests/test_hasher.py
Normal 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
198
tests/test_lict.py
Normal 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
70
tests/test_nsp0.py
Normal 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
123
tests/test_sm2.py
Normal 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
35
tests/test_textproc.py
Normal 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
|
||||
Reference in New Issue
Block a user