180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
import pytest
|
|
import logging
|
|
from configparser import ConfigParser
|
|
from threading import Event
|
|
|
|
from appengine import CommandsLoader
|
|
|
|
|
|
SIMPLE_MODULE = """\
|
|
from appengine import Commands
|
|
|
|
class {classname}(Commands):
|
|
def cmd_hello(self):
|
|
\"\"\"Say hello.\"\"\"
|
|
return "hello"
|
|
"""
|
|
|
|
THREADED_MODULE = """\
|
|
from appengine import Commands
|
|
|
|
class {classname}(Commands):
|
|
def __init__(self, config, log):
|
|
super().__init__(config, log)
|
|
self.threaded = True
|
|
|
|
def run(self):
|
|
while not self.stopped:
|
|
pass
|
|
|
|
def cmd_ping(self):
|
|
return "pong"
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def log():
|
|
return logging.getLogger("test_loader")
|
|
|
|
|
|
@pytest.fixture
|
|
def basic_config():
|
|
config = ConfigParser()
|
|
config["general"] = {}
|
|
return config
|
|
|
|
|
|
def make_loader(tmp_path, config, log):
|
|
stop_event = Event()
|
|
loader = CommandsLoader(stop_event, config, log, str(tmp_path))
|
|
return loader, stop_event
|
|
|
|
|
|
class TestLoadCommands:
|
|
def test_loads_matching_file(self, tmp_path, basic_config, log):
|
|
(tmp_path / "cmds_hello.py").write_text(SIMPLE_MODULE.format(classname="Hello"))
|
|
loader, _ = make_loader(tmp_path, basic_config, log)
|
|
assert "hello" in loader.cmods
|
|
|
|
def test_ignores_non_prefixed_files(self, tmp_path, basic_config, log):
|
|
(tmp_path / "not_a_module.py").write_text(SIMPLE_MODULE.format(classname="NotA"))
|
|
loader, _ = make_loader(tmp_path, basic_config, log)
|
|
assert len(loader.cmods) == 0
|
|
|
|
def test_nickname_overrides_filename(self, tmp_path, log):
|
|
(tmp_path / "cmds_mymod.py").write_text("""\
|
|
from appengine import Commands
|
|
|
|
class MyMod(Commands):
|
|
def __init__(self, config, log):
|
|
super().__init__(config, log)
|
|
self.nickname = "custom"
|
|
""")
|
|
config = ConfigParser()
|
|
config["general"] = {}
|
|
loader, _ = make_loader(tmp_path, config, log)
|
|
assert "custom" in loader.cmods
|
|
|
|
def test_loads_multiple_modules(self, tmp_path, basic_config, log):
|
|
(tmp_path / "cmds_alpha.py").write_text(SIMPLE_MODULE.format(classname="Alpha"))
|
|
(tmp_path / "cmds_beta.py").write_text(SIMPLE_MODULE.format(classname="Beta"))
|
|
loader, _ = make_loader(tmp_path, basic_config, log)
|
|
assert "alpha" in loader.cmods
|
|
assert "beta" in loader.cmods
|
|
|
|
def test_module_config_section_passed(self, tmp_path, log):
|
|
(tmp_path / "cmds_configured.py").write_text("""\
|
|
from appengine import Commands
|
|
|
|
class Configured(Commands):
|
|
def cmd_get_setting(self):
|
|
return self.config.get("my_key")
|
|
""")
|
|
config = ConfigParser()
|
|
config["general"] = {}
|
|
config["configured"] = {"my_key": "my_value"}
|
|
loader, _ = make_loader(tmp_path, config, log)
|
|
assert loader.cmods["configured"].config["my_key"] == "my_value"
|
|
|
|
|
|
class TestSetCmods:
|
|
def test_cmods_shared_across_all_modules(self, tmp_path, basic_config, log):
|
|
(tmp_path / "cmds_a.py").write_text(SIMPLE_MODULE.format(classname="A"))
|
|
(tmp_path / "cmds_b.py").write_text(SIMPLE_MODULE.format(classname="B"))
|
|
loader, _ = make_loader(tmp_path, basic_config, log)
|
|
for v in loader.cmods.values():
|
|
assert set(v.cmods.keys()) == {"a", "b"}
|
|
|
|
|
|
class TestLoadDependencies:
|
|
def test_list_dependency_injected(self, tmp_path, log):
|
|
(tmp_path / "cmds_provider.py").write_text(SIMPLE_MODULE.format(classname="Provider"))
|
|
(tmp_path / "cmds_consumer.py").write_text("""\
|
|
from appengine import Commands
|
|
|
|
class Consumer(Commands):
|
|
dependencies = ["provider"]
|
|
|
|
def cmd_test(self):
|
|
return "ok"
|
|
""")
|
|
config = ConfigParser()
|
|
config["general"] = {}
|
|
loader, _ = make_loader(tmp_path, config, log)
|
|
loader._load_dependencies()
|
|
assert hasattr(loader.cmods["consumer"], "provider")
|
|
assert loader.cmods["consumer"].provider is loader.cmods["provider"]
|
|
|
|
def test_dict_dependency_injected(self, tmp_path, log):
|
|
(tmp_path / "cmds_svc.py").write_text(SIMPLE_MODULE.format(classname="Svc"))
|
|
(tmp_path / "cmds_client.py").write_text("""\
|
|
from appengine import Commands
|
|
|
|
class Client(Commands):
|
|
dependencies = {"my_svc": "svc"}
|
|
|
|
def cmd_test(self):
|
|
return "ok"
|
|
""")
|
|
config = ConfigParser()
|
|
config["general"] = {}
|
|
loader, _ = make_loader(tmp_path, config, log)
|
|
loader._load_dependencies()
|
|
assert hasattr(loader.cmods["client"], "my_svc")
|
|
assert loader.cmods["client"].my_svc is loader.cmods["svc"]
|
|
|
|
def test_missing_dependency_logs_error(self, tmp_path, log, caplog):
|
|
(tmp_path / "cmds_orphan.py").write_text("""\
|
|
from appengine import Commands
|
|
|
|
class Orphan(Commands):
|
|
dependencies = ["nonexistent"]
|
|
|
|
def cmd_test(self):
|
|
return "ok"
|
|
""")
|
|
config = ConfigParser()
|
|
config["general"] = {}
|
|
loader, _ = make_loader(tmp_path, config, log)
|
|
with caplog.at_level(logging.ERROR, logger="test_loader"):
|
|
loader._load_dependencies()
|
|
assert "nonexistent" in caplog.text
|
|
|
|
|
|
class TestLifecycle:
|
|
def test_start_and_stop_threaded_module(self, tmp_path, log):
|
|
(tmp_path / "cmds_worker.py").write_text(THREADED_MODULE.format(classname="Worker"))
|
|
config = ConfigParser()
|
|
config["general"] = {}
|
|
loader, stop_event = make_loader(tmp_path, config, log)
|
|
loader.start()
|
|
assert loader.cmods["worker"].is_alive()
|
|
loader.stop()
|
|
loader.join()
|
|
assert not loader.cmods["worker"].is_alive()
|
|
|
|
def test_free_does_not_raise(self, tmp_path, basic_config, log):
|
|
(tmp_path / "cmds_simple.py").write_text(SIMPLE_MODULE.format(classname="Simple"))
|
|
loader, _ = make_loader(tmp_path, basic_config, log)
|
|
loader.free() # must not raise
|