+
+
Frequency
+
+
+
+
+
+
+
+
+
+
+
+
+
Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Protocols
+
+ rtlamr decodes utility meter transmissions (water, gas, electric) using ERT protocol.
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
new file mode 100644
index 0000000..27e4922
--- /dev/null
+++ b/tests/test_requirements.py
@@ -0,0 +1,109 @@
+import pytest
+from pathlib import Path
+import importlib.metadata
+import tomllib # Standard in Python 3.11+
+
+def get_root_path():
+ return Path(__file__).parent.parent
+
+def _clean_string(req):
+ """Normalizes a requirement string (lowercase and removes spaces)."""
+ return req.strip().lower().replace(" ", "")
+
+def parse_txt_requirements(file_path):
+ """Extracts full requirement strings (name + version) from a .txt file."""
+ if not file_path.exists():
+ return set()
+ packages = set()
+ with open(file_path, "r") as f:
+ for line in f:
+ line = line.strip()
+ # Ignore empty lines, comments, and recursive/local flags
+ if not line or line.startswith(("#", "-e", "git+", "-r")):
+ continue
+ packages.add(_clean_string(line))
+ return packages
+
+def parse_toml_section(data, section_type="main"):
+ """Extracts full requirement strings from pyproject.toml."""
+ packages = set()
+ if section_type == "main":
+ deps = data.get("project", {}).get("dependencies", [])
+ else:
+ # Check optional-dependencies or dependency-groups
+ deps = data.get("project", {}).get("optional-dependencies", {}).get("dev", [])
+ if not deps:
+ deps = data.get("dependency-groups", {}).get("dev", [])
+
+ for req in deps:
+ packages.add(_clean_string(req))
+ return packages
+
+def test_dependency_files_integrity():
+ """1. Verifies that .txt files and pyproject.toml have identical names AND versions."""
+ root = get_root_path()
+ toml_path = root / "pyproject.toml"
+ assert toml_path.exists(), "Missing pyproject.toml"
+
+ with open(toml_path, "rb") as f:
+ toml_data = tomllib.load(f)
+
+ # Validate Production Sync
+ txt_main = parse_txt_requirements(root / "requirements.txt")
+ toml_main = parse_toml_section(toml_data, "main")
+ assert txt_main == toml_main, (
+ f"Production version mismatch!\n"
+ f"Only in TXT: {txt_main - toml_main}\n"
+ f"Only in TOML: {toml_main - txt_main}"
+ )
+
+ # Validate Development Sync
+ txt_dev = parse_txt_requirements(root / "requirements-dev.txt")
+ toml_dev = parse_toml_section(toml_data, "dev")
+ assert txt_dev == toml_dev, (
+ f"Development version mismatch!\n"
+ f"Only in TXT: {txt_dev - toml_dev}\n"
+ f"Only in TOML: {toml_dev - txt_dev}"
+ )
+
+def test_environment_vs_toml():
+ """2. Verifies that installed packages satisfy TOML requirements."""
+ root = get_root_path()
+ with open(root / "pyproject.toml", "rb") as f:
+ data = tomllib.load(f)
+
+ all_declared = parse_toml_section(data, "main") | parse_toml_section(data, "dev")
+ _verify_installation(all_declared, "TOML")
+
+def test_environment_vs_requirements():
+ """3. Verifies that installed packages satisfy .txt requirements."""
+ root = get_root_path()
+ all_txt_deps = (
+ parse_txt_requirements(root / "requirements.txt") |
+ parse_txt_requirements(root / "requirements-dev.txt")
+ )
+ _verify_installation(all_txt_deps, "requirements.txt")
+
+def _verify_installation(package_set, source_name):
+ """Helper to check if declared versions match installed versions."""
+ missing_or_wrong = []
+
+ for req in package_set:
+ # Split name from version to check installation status
+ # handles ==, >=, ~=, <=, > , <
+ import re
+ parts = re.split(r'==|>=|~=|<=|>|<', req)
+ name = parts[0].strip()
+
+ try:
+ installed_ver = importlib.metadata.version(name)
+ # If the config uses exact versioning '==', we can do a strict check
+ if "==" in req:
+ expected_ver = req.split("==")[1].strip()
+ if installed_ver != expected_ver:
+ missing_or_wrong.append(f"{name} (Installed: {installed_ver}, Expected: {expected_ver})")
+ except importlib.metadata.PackageNotFoundError:
+ missing_or_wrong.append(f"{name} (Not installed)")
+
+ if missing_or_wrong:
+ pytest.fail(f"Environment out of sync with {source_name}:\n" + "\n".join(missing_or_wrong))
\ No newline at end of file