This commit is contained in:
Jonas Bengtsson
2026-10-03 18:54:37 +02:00
parent ba8df075c3
commit 1aa7aa8767
13 changed files with 1167 additions and 30 deletions
+197
View File
@@ -0,0 +1,197 @@
"""CLI tests: env-var configuration and bad-argument handling.
The serve coroutine blocks on a shutdown signal (by design — it is a
service), so serve-args tests capture the parsed args instead of
running the real server loop.
"""
from __future__ import annotations
import pytest
from wago_sim.cli import _env, _env_port, main
class TestEnvHelpers:
def test_port_from_env(self, monkeypatch):
monkeypatch.setenv("WAGO_SIM_PORT", "1234")
assert _env_port("WAGO_SIM_PORT", 502) == 1234
monkeypatch.delenv("WAGO_SIM_PORT")
assert _env_port("WAGO_SIM_PORT", 502) == 502
def test_host_from_env(self, monkeypatch):
monkeypatch.setenv("WAGO_SIM_HOST", "10.0.0.5")
assert _env("WAGO_SIM_HOST", "0.0.0.0") == "10.0.0.5"
monkeypatch.delenv("WAGO_SIM_HOST")
assert _env("WAGO_SIM_HOST", "0.0.0.0") == "0.0.0.0"
class TestServeEnvConfig:
"""WAGO_SIM_* env vars configure serve without CLI args (service mode)."""
@staticmethod
def _capture_serve(monkeypatch):
"""Replace the serve coroutine with an args recorder.
main() builds its parser on each call and resolves `_serve` from
the module namespace, so monkeypatching before the call works.
"""
captured: dict = {}
async def fake_serve(args) -> int:
captured["node"] = args.node
captured["host"] = args.host
captured["port"] = args.port
return 0
monkeypatch.setattr("wago_sim.cli._serve", fake_serve)
return captured
def test_node_and_port_from_env(self, monkeypatch):
captured = self._capture_serve(monkeypatch)
monkeypatch.setenv("WAGO_SIM_NODE", "config/pump_node.yaml")
monkeypatch.setenv("WAGO_SIM_PORT", "15025")
monkeypatch.setenv("WAGO_SIM_HOST", "127.0.0.1")
assert main(["serve"]) == 0
assert captured["node"] == "config/pump_node.yaml"
assert captured["port"] == 15025
assert captured["host"] == "127.0.0.1"
def test_cli_overrides_env(self, monkeypatch):
captured = self._capture_serve(monkeypatch)
monkeypatch.setenv("WAGO_SIM_NODE", "config/other.yaml")
monkeypatch.setenv("WAGO_SIM_PORT", "1111")
assert main(["serve", "--node", "config/pump_node.yaml",
"--port", "15026"]) == 0
assert captured["node"] == "config/pump_node.yaml"
assert captured["port"] == 15026
def test_missing_node_is_an_error(self, monkeypatch):
monkeypatch.delenv("WAGO_SIM_NODE", raising=False)
with pytest.raises(SystemExit) as excinfo:
main(["serve"])
# argparse parser.error exit code
assert excinfo.value.code == 2
def test_node_env_only_no_cli_args(self, monkeypatch):
"""The container CMD is plain `wago-sim serve` — everything env."""
captured = self._capture_serve(monkeypatch)
monkeypatch.setenv("WAGO_SIM_NODE", "config/pump_node.yaml")
assert main(["serve"]) == 0
assert captured["node"] == "config/pump_node.yaml"
# host/port fall back to defaults
assert captured["host"] == "0.0.0.0"
assert captured["port"] == 502
class TestBadArgumentHandling:
"""Bad arguments produce clean errors, exit code 2, no tracebacks."""
def test_no_command_is_usage_error(self):
with pytest.raises(SystemExit) as excinfo:
main([])
assert excinfo.value.code == 2
def test_unknown_command_is_usage_error(self):
with pytest.raises(SystemExit) as excinfo:
main(["frobnicate"])
assert excinfo.value.code == 2
def test_unknown_option_is_usage_error(self):
with pytest.raises(SystemExit) as excinfo:
main(["serve", "--frobnicate"])
assert excinfo.value.code == 2
def test_non_numeric_port_rejected_at_parse(self):
with pytest.raises(SystemExit) as excinfo:
main(["serve", "--node", "config/pump_node.yaml", "--port", "abc"])
assert excinfo.value.code == 2
@pytest.mark.parametrize("port", ["0", "-1", "65536", "99999"])
def test_port_out_of_range_rejected(self, port):
with pytest.raises(SystemExit) as excinfo:
main(["serve", "--node", "config/pump_node.yaml", "--port", port])
assert excinfo.value.code == 2
def test_env_port_invalid_rejected(self, monkeypatch, capsys):
monkeypatch.setenv("WAGO_SIM_PORT", "banana")
monkeypatch.delenv("WAGO_SIM_NODE", raising=False)
with pytest.raises(SystemExit) as excinfo:
main(["serve"])
assert excinfo.value.code == 2
assert "WAGO_SIM_PORT" in capsys.readouterr().err
def test_env_port_out_of_range_rejected(self, monkeypatch):
monkeypatch.setenv("WAGO_SIM_PORT", "70000")
monkeypatch.delenv("WAGO_SIM_NODE", raising=False)
with pytest.raises(SystemExit) as excinfo:
main(["serve"])
assert excinfo.value.code == 2
def test_missing_node_file_is_clean_error(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main(["serve", "--node", "config/does_not_exist.yaml"])
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "wago-sim: error:" in err
assert "node config file not found" in err
assert "Traceback" not in err
def test_missing_scenario_file_is_clean_error(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main(["validate", "--scenario", "scenarios/nope.yaml"])
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "scenario file not found" in err
assert "Traceback" not in err
def test_invalid_yaml_is_clean_error(self, capsys, tmp_path):
bad = tmp_path / "bad.yaml"
bad.write_text("key: [unclosed", encoding="utf-8")
with pytest.raises(SystemExit) as excinfo:
main(["validate", "--scenario", str(bad)])
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "not valid YAML" in err
assert "Traceback" not in err
def test_directory_as_yaml_is_clean_error(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main(["validate", "--scenario", "scenarios"])
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "not a file" in err
assert "Traceback" not in err
def test_run_missing_required_options_is_usage_error(self):
with pytest.raises(SystemExit) as excinfo:
main(["run"])
assert excinfo.value.code == 2
class TestReportPathHandling:
def test_report_dirs_auto_created(self, tmp_path, capsys):
"""Missing report parent directories are created before the run."""
from wago_sim.cli import _prepare_report_path
target = tmp_path / "a" / "b" / "r.jsonl"
result = _prepare_report_path(str(target), "--report-jsonl")
assert result.parent.is_dir()
def test_unwritable_report_path_is_clean_error(self, tmp_path, capsys):
from wago_sim.cli import _prepare_report_path
# a *file* used as parent directory -> cannot create
blocker = tmp_path / "blocker"
blocker.write_text("x", encoding="utf-8")
target = blocker / "sub" / "r.jsonl"
with pytest.raises(SystemExit) as excinfo:
_prepare_report_path(str(target), "--report-jsonl")
assert excinfo.value.code == 2
assert "cannot create directory" in capsys.readouterr().err
def test_missing_node_is_an_error(self, monkeypatch):
monkeypatch.delenv("WAGO_SIM_NODE", raising=False)
with pytest.raises(SystemExit) as excinfo:
main(["serve"])
assert excinfo.value.code == 2 # argparse error exit code