From e464ff942b87d4b8c8590452b7108d3371b56c78 Mon Sep 17 00:00:00 2001 From: novirium Date: Wed, 25 Mar 2020 13:45:04 +0800 Subject: [PATCH] Core tests --- shepherd/agent/control.py | 6 ++- shepherd/agent/core.py | 20 ++++++++- tests/test_control.py | 1 - tests/test_core.py | 94 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 tests/test_core.py diff --git a/shepherd/agent/control.py b/shepherd/agent/control.py index 83e6c19..80a4ec8 100644 --- a/shepherd/agent/control.py +++ b/shepherd/agent/control.py @@ -42,9 +42,13 @@ def control_confspec(): class CoreUpdateState(): + """ + A container for all state that might need communicating remotely to Control. Abstracts the + Statesman topics away from other parts of the Agent. + """ def __init__(self, cmd_reader, cmd_result_writer): """ - Control update handler for the `/update` core endpoint. + Control update handler for the `/update` core endpoint. Needs a reference to the CommandRunner """ self.topic_bundle = statesman.TopicBundle({ 'status': statesman.StateWriter(), diff --git a/shepherd/agent/core.py b/shepherd/agent/core.py index 53fbd4c..3676e3e 100644 --- a/shepherd/agent/core.py +++ b/shepherd/agent/core.py @@ -78,6 +78,8 @@ class Agent(): def start(self): + # After this point, plugins may already have their own threads running if they create + # them during init plugin_interfaces, self.interface_functions = plugin.init_plugins( self.plugin_configs, self.core_config, {}) @@ -97,6 +99,19 @@ class Agent(): log.warning("Shepherd control config section not present. Will not attempt to" " connect to Shepherd Control server.") + # Shift Control check to when it actually tries to find config, and just set + # control_enabled to false. + # Does all the other cmd_runner and update state stuff need to happen still if + # control is disabled? + # Should Core really need to know anything about the command runner, or should it + # just hand the interface functions in directly to Control? + + # Need somewhere to eventually pass in the hooks Tasks will need for the lowpower stuff, + # probably just another init_plugins arg. + # Eventually when the dust settles we might revisit converting the core "shepherd" + # namespace stuff into it's own plugin interface, as it's using a lot of the same + # mechanisms, but we're having to pass it all around individually. + # tasks.init_tasks(self.core_config) # seperate tasks.start? # plugin.start() # Run the plugin `.run` hooks in seperate threads @@ -226,8 +241,9 @@ def core_confspec(): confspec.add_specs(optional=True, spec_dict={ "root_dir": - (StringSpec(helptext="Operating directory for shepherd to place working files."), - "./shepherd"), + (StringSpec(helptext="Operating directory for shepherd to place working files." + " Relative to the directory containing the default config file."), + "./"), "custom_config_path": StringSpec(helptext="Path to custom config layer TOML file."), "compiled_config_path": diff --git a/tests/test_control.py b/tests/test_control.py index dc73748..4a03bfb 100644 --- a/tests/test_control.py +++ b/tests/test_control.py @@ -186,7 +186,6 @@ def test_command_runner(): wr_msg = list(cmd_runner.cmd_result_writer._messages.values())[-1] assert wr_msg == [15, 9] - # Control/Plugin integration tests # Test command_runner with actual plugin diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..3007d0b --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,94 @@ +# pylint: disable=redefined-outer-name +from pathlib import Path +import logging + +import pytest + +from shepherd.agent import core + + +@pytest.fixture +def local_agent(): + return core.Agent(control_enabled=False) + + +@pytest.fixture +def basic_config(tmp_path): + def_conf_file = tmp_path / "shepherd_default.toml" + def_conf_file.write_text(""" +[shepherd] +name = "shepherd-test" +""") + return def_conf_file + + +@pytest.fixture +def custom_config(tmp_path): + def_conf_file = tmp_path / "shepherd_default.toml" + def_conf_file.write_text(""" +[shepherd] +name = "shepherd-test" +custom_config_path = "shepherd_custom.toml" +""") + custom_conf_file = tmp_path / "shepherd_custom.toml" + custom_conf_file.write_text(""" +[shepherd] +name = "shepherd-custom" +""") + return def_conf_file + + +@pytest.fixture +def plugin_config(tmp_path, request): + plugin_dir = Path(request.fspath.dirname)/'assets' + def_conf_file = tmp_path / "shepherd_default.toml" + def_conf_file.write_text(F""" +[shepherd] +name = "shepherd-test" +plugin_dir = "{plugin_dir}" +[classtestplugin] +spec1 = "asdf" +""") + return def_conf_file + + +def test_local_agent(local_agent): + pass + + +def test_local_agent_load(local_agent, basic_config): + local_agent.load(basic_config) + + +def test_local_compiled_conf(local_agent, basic_config): + local_agent.load(basic_config) + compiled_conf = (basic_config.parent / "compiled-config.toml").read_text() + assert 'name = "shepherd-test"' in compiled_conf + # Paths should be resolved to absolute + assert 'plugin_dir = "/' in compiled_conf + assert 'Compiled Shepherd config' in compiled_conf + + +def test_custom_conf_load(local_agent, custom_config): + local_agent.load(custom_config) + assert local_agent.core_config["name"] == "shepherd-custom" + + +def test_new_device_trigger(local_agent, custom_config, caplog): + caplog.set_level(logging.INFO) + (custom_config.parent / "shepherd.new").touch() + local_agent.load(custom_config) + assert "'new device' mode enabled" in caplog.text + assert (custom_config.parent / "shepherd.identity").exists() + + +def test_local_agent_start(local_agent, basic_config): + local_agent.load(basic_config) + local_agent.start() + + +def test_local_agent_plugin_start(local_agent, plugin_config): + local_agent.load(plugin_config) + local_agent.start() + assert local_agent.interface_functions["classtestplugin"].instance_method( + 3) == "instance method 3"